Skip to main content

atmos/os_lib/layout/
boxmodel.rs

1// 分割: layout.rs より機械的に移動(2026-07-16 リファクタ フェーズ4)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// margin の左右が auto か(`margin:auto` / `margin:0 auto` / `margin-left:auto`)。
6pub(crate) fn margin_is_h_auto(node: &StyledNode) -> bool {
7    if node
8        .value_ref("margin-left")
9        .map(|v| v.trim() == "auto")
10        .unwrap_or(false)
11    {
12        return true;
13    }
14    if let Some(m) = node.value_ref("margin") {
15        let mut it = m.split_whitespace();
16        let first = it.next();
17        let second = it.next();
18        let lr = second.or(first);
19        return lr.map(|s| s == "auto").unwrap_or(false);
20    }
21    false
22}
23
24/// flex アイテムの主軸方向の auto margin 個数を返す(row: left/right、column: top/bottom)。
25/// `margin-<side>:auto` 明示、または `margin` ショートハンドの該当辺が auto のとき数える。
26pub(crate) fn box_main_auto_margins(b: &LayoutBox, is_row: bool) -> (bool, bool) {
27    fn side_auto(b: &LayoutBox, side: &str, short_idx_4: usize) -> bool {
28        if let Some(v) = box_style_value(b, side) {
29            return v.trim() == "auto";
30        }
31        if let Some(m) = box_style_value(b, "margin") {
32            let mut toks = [""; 4];
33            let mut count = 0;
34            for tok in m.split_whitespace().take(4) {
35                toks[count] = tok;
36                count += 1;
37            }
38            // margin ショートハンド: 1=all, 2=(v,h), 3=(t,h,b), 4=(t,r,b,l)
39            let val = match count {
40                1 => Some(toks[0]),
41                2 => {
42                    // top/bottom=0, left/right=1
43                    if short_idx_4 == 0 || short_idx_4 == 2 {
44                        Some(toks[0])
45                    } else {
46                        Some(toks[1])
47                    }
48                }
49                3 => match short_idx_4 {
50                    0 => Some(toks[0]),
51                    2 => Some(toks[2]),
52                    _ => Some(toks[1]), // left/right
53                },
54                4 => Some(toks[short_idx_4]),
55                _ => None,
56            };
57            return val.map(|s| s == "auto").unwrap_or(false);
58        }
59        false
60    }
61    if is_row {
62        // top=0,right=1,bottom=2,left=3
63        (
64            side_auto(b, "margin-left", 3),
65            side_auto(b, "margin-right", 1),
66        )
67    } else {
68        (
69            side_auto(b, "margin-top", 0),
70            side_auto(b, "margin-bottom", 2),
71        )
72    }
73}
74
75/// 要素の position 値(小文字化。未指定は "static")。
76/// `position` の値(既知の 5 種。それ以外・未指定は "static")。
77/// 【2026-10-19】レイアウトの最頻出の呼び出しの 1 つで、従来は毎回小文字化した `String` を作っていた。
78pub(crate) fn box_position(b: &LayoutBox) -> &'static str {
79    let Some(v) = box_style_value_ref(b, "position") else { return "static" };
80    let v = v.trim();
81    if v.eq_ignore_ascii_case("relative") {
82        "relative"
83    } else if v.eq_ignore_ascii_case("absolute") {
84        "absolute"
85    } else if v.eq_ignore_ascii_case("fixed") {
86        "fixed"
87    } else if v.eq_ignore_ascii_case("sticky") {
88        "sticky"
89    } else {
90        "static"
91    }
92}
93
94/// `direction:rtl` のとき論理方向キーワード(inline-start/inline-end)を物理方向
95/// (left/right)へ解決する。`direction` は継承プロパティとして既に解決済みのため、
96/// このボックス自身の `specified_values` を見るだけで祖先からの継承値も反映される。
97pub(crate) fn box_is_rtl(b: &LayoutBox) -> bool {
98    box_style_value(b, "direction")
99        .map(|v| v.trim().eq_ignore_ascii_case("rtl"))
100        .unwrap_or(false)
101}
102
103/// 要素の float 値。"left" / "right"(物理方向は direction に関わらず不変)に加え、
104/// CSS Logical Properties の `inline-start`/`inline-end`(direction に応じて
105/// left/right へ解決される)にも対応する。それ以外は "none" 扱い。
106pub(crate) fn box_float(b: &LayoutBox) -> &'static str {
107    let rtl = box_is_rtl(b);
108    match box_style_value(b, "float").map(|v| v.trim().to_lowercase()) {
109        Some(ref v) if v == "left" => "left",
110        Some(ref v) if v == "right" => "right",
111        Some(ref v) if v == "inline-start" => {
112            if rtl {
113                "right"
114            } else {
115                "left"
116            }
117        }
118        Some(ref v) if v == "inline-end" => {
119            if rtl {
120                "left"
121            } else {
122                "right"
123            }
124        }
125        _ => "none",
126    }
127}
128
129/// 要素の clear 値。"left" / "right" / "both" に加え、`float` と同様に
130/// `inline-start`/`inline-end` の論理方向キーワードにも対応する。
131pub(crate) fn box_clear(b: &LayoutBox) -> &'static str {
132    let rtl = box_is_rtl(b);
133    match box_style_value(b, "clear").map(|v| v.trim().to_lowercase()) {
134        Some(ref v) if v == "left" => "left",
135        Some(ref v) if v == "right" => "right",
136        Some(ref v) if v == "both" => "both",
137        Some(ref v) if v == "inline-start" => {
138            if rtl {
139                "right"
140            } else {
141                "left"
142            }
143        }
144        Some(ref v) if v == "inline-end" => {
145            if rtl {
146                "left"
147            } else {
148                "right"
149            }
150        }
151        _ => "none",
152    }
153}
154
155/// 長さプロパティを base(%解決の基準)で解決。auto/未指定は None。
156pub(crate) fn box_len(b: &LayoutBox, prop: &str, base: i32) -> Option<i32> {
157    parse_length_value(box_style_value(b, prop)).and_then(|lv| resolve_length_value(lv, base))
158}
159
160/// `top`/`right`/`bottom`/`left`(`side` はその名前そのもの)を解決する。個別の longhand が
161/// 明示されていればそれを優先し、無ければ `inset` ショートハンド(margin と同じ1〜4値展開)
162/// から対応する辺を取り出してフォールバックする。
163pub(crate) fn box_inset_len(b: &LayoutBox, side: &str, base: i32) -> Option<i32> {
164    if let Some(v) = box_len(b, side, base) {
165        return Some(v);
166    }
167    let side_idx = match side {
168        "top" => 0,
169        "right" => 1,
170        "bottom" => 2,
171        "left" => 3,
172        _ => return None,
173    };
174    let inset = box_style_value(b, "inset")?;
175    let mut toks = [""; 4];
176    let mut count = 0;
177    for t in inset.split_whitespace().take(4) {
178        toks[count] = t;
179        count += 1;
180    }
181    let tok = match count {
182        1 => Some(toks[0]),
183        2 => match side_idx {
184            0 | 2 => Some(toks[0]),
185            _ => Some(toks[1]),
186        },
187        3 => match side_idx {
188            0 => Some(toks[0]),
189            2 => Some(toks[2]),
190            _ => Some(toks[1]),
191        },
192        4 => Some(toks[side_idx]),
193        _ => None,
194    }?;
195    parse_length_value(Some(String::from(tok))).and_then(|lv| resolve_length_value(lv, base))
196}
197
198/// `name(...)` の括弧内文字列を取り出す(`string_slice` lint 回避で str::get のみ使用)。
199pub(crate) fn extract_fn_args(s: &str, name: &str) -> Option<String> {
200    let needle = alloc::format!("{}(", name);
201    let pos = s.find(&needle)?;
202    let after = s.get(pos + needle.len()..)?;
203    let end = after.find(')')?;
204    after.get(..end).map(String::from)
205}
206
207/// translate の 1 引数(px / % / rem / vh / vw)を base 基準で解決。
208/// 以前は `px`/`%` の2種のみ対応で、それ以外の単位(`rem`/`vh`/`vw`等)は
209/// `t.strip_suffix("px").unwrap_or(t)`が単位付き文字列をそのまま数値
210/// パースへ渡してしまい、静かに`0`へフォールバックしていた——単に
211/// 「対応外」なのではなく、`transform: translate(1rem, 10px)`のような
212/// 式で意図した移動量が消えて要素が動かなくなる、実害のあるバグだった。
213/// `em`(要素自身の算出フォントサイズ基準)はこの関数の引数だけでは
214/// 算出フォントサイズを解決できないため対象外のまま
215/// (`font-size`の継承解決には別途プロパティ探索が必要で、誤った既定値
216/// 〔例:常に16px〕を当てはめると`rem`と区別が付かない別の壊れ方になる
217/// リスクがあり見送り)。2026-07-17 発見・実装。
218pub(crate) fn resolve_translate_len(tok: &str, base: i32) -> i32 {
219    let t = tok.trim();
220    if let Some(p) = t.strip_suffix('%') {
221        return p
222            .trim()
223            .parse::<f32>()
224            .map(|v| (v / 100.0 * base as f32) as i32)
225            .unwrap_or(0);
226    }
227    if let Some(p) = t.strip_suffix("rem") {
228        return p.trim().parse::<f32>().map(|v| (v * 16.0) as i32).unwrap_or(0);
229    }
230    if let Some(p) = t.strip_suffix("vh") {
231        let h = super::values::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
232        return p
233            .trim()
234            .parse::<f32>()
235            .map(|v| (v / 100.0 * h as f32) as i32)
236            .unwrap_or(0);
237    }
238    if let Some(p) = t.strip_suffix("vw") {
239        let w = super::values::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
240        return p
241            .trim()
242            .parse::<f32>()
243            .map(|v| (v / 100.0 * w as f32) as i32)
244            .unwrap_or(0);
245    }
246    let n = t.strip_suffix("px").unwrap_or(t).trim();
247    n.parse::<f32>().map(|v| v as i32).unwrap_or(0)
248}
249
250/// `transform` から translate オフセット (dx, dy) を取り出す。% は要素自身の (w,h) 基準。
251/// translate(x[,y]) / translateX(x) / translateY(y) に対応(scale/rotate は未対応・無視)。
252pub(crate) fn box_translate(b: &LayoutBox) -> (i32, i32) {
253    let t = match box_style_value(b, "transform") {
254        Some(v) => v,
255        None => return (0, 0),
256    };
257    let w = b.dimensions.content.width;
258    let h = b.dimensions.content.height;
259    if let Some(args) = extract_fn_args(&t, "translate") {
260        let mut parts = args.split(',');
261        let dx = parts
262            .next()
263            .map(|a| resolve_translate_len(a, w))
264            .unwrap_or(0);
265        let dy = parts
266            .next()
267            .map(|a| resolve_translate_len(a, h))
268            .unwrap_or(0);
269        return (dx, dy);
270    }
271    let dx = extract_fn_args(&t, "translateX")
272        .map(|a| resolve_translate_len(&a, w))
273        .unwrap_or(0);
274    let dy = extract_fn_args(&t, "translateY")
275        .map(|a| resolve_translate_len(&a, h))
276        .unwrap_or(0);
277    (dx, dy)
278}
279
280/// グリッド占有 (rowspan/colspan 解決用) のヘルパ。
281pub(crate) fn occ_is(occ: &[Vec<bool>], r: usize, c: usize) -> bool {
282    occ.get(r)
283        .and_then(|row| row.get(c))
284        .copied()
285        .unwrap_or(false)
286}
287pub(crate) fn occ_mark(occ: &mut Vec<Vec<bool>>, r: usize, c: usize) {
288    while occ.len() <= r {
289        occ.push(Vec::new());
290    }
291    let row = &mut occ[r];
292    while row.len() <= c {
293        row.push(false);
294    }
295    row[c] = true;
296}
297
298// レイアウトツリーの構築と計算