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)。
59pub fn parse_len_with_font(tok: &str, font_px: f32) -> 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") {
65        return n.parse::<f32>().ok().filter(|v| v.is_finite()).map(|v| (v * 16.0) as i32);
66    }
67    if let Some(n) = t.strip_suffix("em") {
68        let f = if font_px > 0.0 { font_px } else { 16.0 };
69        return n.parse::<f32>().ok().filter(|v| v.is_finite()).map(|v| (v * f) as i32);
70    }
71    // 単位なしは 0 のときだけ長さとして認める(CSS の規則)。
72    match t.parse::<f32>() {
73        Ok(0.0) => Some(0),
74        _ => None,
75    }
76}
77
78pub fn parse_len(tok: &str) -> Option<i32> {
79    parse_len_with_font(tok, 16.0)
80}
81
82/// 影 1 個を解析する(`inset`、長さ 2〜4 個、色は順不同)。長さが 2 個未満なら無効。
83/// 4 個目(spread)は読み飛ばす(未対応)。
84pub fn parse_one_with_font(seg: &str, font_px: f32) -> Option<Shadow> {
85    let mut lens: Vec<i32> = Vec::new();
86    let mut color = DEFAULT_SHADOW_COLOR;
87    let mut inset = false;
88    for tok in split_top_level_ws(seg.trim()) {
89        if tok.eq_ignore_ascii_case("inset") {
90            inset = true;
91        } else if let Some(n) = parse_len_with_font(tok, font_px) {
92            lens.push(n);
93        } else if let Some(c) = crate::os_lib::css::parse_color(tok) {
94            color = c;
95        }
96    }
97    if lens.len() < 2 {
98        return None;
99    }
100    let blur = lens.get(2).copied().unwrap_or(0).max(0);
101    Some((lens[0], lens[1], blur, color, inset))
102}
103
104pub fn parse_one(seg: &str) -> Option<Shadow> {
105    parse_one_with_font(seg, 16.0)
106}
107
108/// `box-shadow` の値全体(カンマ区切りの複数影)を解析する。`none` は空。
109pub fn parse_list_with_font(v: &str, font_px: f32) -> Vec<Shadow> {
110    let v = v.trim();
111    if v.is_empty() || v.eq_ignore_ascii_case("none") {
112        return Vec::new();
113    }
114    crate::os_lib::css::split_top_level_commas(v)
115        .into_iter()
116        .filter_map(|s| parse_one_with_font(s, font_px))
117        .collect()
118}
119
120pub fn parse_list(v: &str) -> Vec<Shadow> {
121    parse_list_with_font(v, 16.0)
122}
123
124/// 影の矩形の縁からの距離 `d`(外側が正、内側が負。px)での濃度 0.0〜1.0。
125///
126/// ぼかしはガウスぼかしの近似として、縁を中心に幅 `blur` の直線勾配にする
127/// (d = -blur/2 で 1、d = +blur/2 で 0)。ぼかし 0 なら内側 1・外側 0。
128pub fn coverage(d: f32, blur: i32) -> f32 {
129    if blur <= 0 {
130        return if d <= 0.0 { 1.0 } else { 0.0 };
131    }
132    (0.5 - d / blur as f32).clamp(0.0, 1.0)
133}