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")。
76pub(crate) fn box_position(b: &LayoutBox) -> String {
77    box_style_value(b, "position")
78        .map(|v| v.trim().to_lowercase())
79        .unwrap_or_else(|| String::from("static"))
80}
81
82/// `direction:rtl` のとき論理方向キーワード(inline-start/inline-end)を物理方向
83/// (left/right)へ解決する。`direction` は継承プロパティとして既に解決済みのため、
84/// このボックス自身の `specified_values` を見るだけで祖先からの継承値も反映される。
85pub(crate) fn box_is_rtl(b: &LayoutBox) -> bool {
86    box_style_value(b, "direction")
87        .map(|v| v.trim().eq_ignore_ascii_case("rtl"))
88        .unwrap_or(false)
89}
90
91/// 要素の float 値。"left" / "right"(物理方向は direction に関わらず不変)に加え、
92/// CSS Logical Properties の `inline-start`/`inline-end`(direction に応じて
93/// left/right へ解決される)にも対応する。それ以外は "none" 扱い。
94pub(crate) fn box_float(b: &LayoutBox) -> &'static str {
95    let rtl = box_is_rtl(b);
96    match box_style_value(b, "float").map(|v| v.trim().to_lowercase()) {
97        Some(ref v) if v == "left" => "left",
98        Some(ref v) if v == "right" => "right",
99        Some(ref v) if v == "inline-start" => {
100            if rtl {
101                "right"
102            } else {
103                "left"
104            }
105        }
106        Some(ref v) if v == "inline-end" => {
107            if rtl {
108                "left"
109            } else {
110                "right"
111            }
112        }
113        _ => "none",
114    }
115}
116
117/// 要素の clear 値。"left" / "right" / "both" に加え、`float` と同様に
118/// `inline-start`/`inline-end` の論理方向キーワードにも対応する。
119pub(crate) fn box_clear(b: &LayoutBox) -> &'static str {
120    let rtl = box_is_rtl(b);
121    match box_style_value(b, "clear").map(|v| v.trim().to_lowercase()) {
122        Some(ref v) if v == "left" => "left",
123        Some(ref v) if v == "right" => "right",
124        Some(ref v) if v == "both" => "both",
125        Some(ref v) if v == "inline-start" => {
126            if rtl {
127                "right"
128            } else {
129                "left"
130            }
131        }
132        Some(ref v) if v == "inline-end" => {
133            if rtl {
134                "left"
135            } else {
136                "right"
137            }
138        }
139        _ => "none",
140    }
141}
142
143/// 長さプロパティを base(%解決の基準)で解決。auto/未指定は None。
144pub(crate) fn box_len(b: &LayoutBox, prop: &str, base: i32) -> Option<i32> {
145    parse_length_value(box_style_value(b, prop)).and_then(|lv| resolve_length_value(lv, base))
146}
147
148/// `top`/`right`/`bottom`/`left`(`side` はその名前そのもの)を解決する。個別の longhand が
149/// 明示されていればそれを優先し、無ければ `inset` ショートハンド(margin と同じ1〜4値展開)
150/// から対応する辺を取り出してフォールバックする。
151pub(crate) fn box_inset_len(b: &LayoutBox, side: &str, base: i32) -> Option<i32> {
152    if let Some(v) = box_len(b, side, base) {
153        return Some(v);
154    }
155    let side_idx = match side {
156        "top" => 0,
157        "right" => 1,
158        "bottom" => 2,
159        "left" => 3,
160        _ => return None,
161    };
162    let inset = box_style_value(b, "inset")?;
163    let mut toks = [""; 4];
164    let mut count = 0;
165    for t in inset.split_whitespace().take(4) {
166        toks[count] = t;
167        count += 1;
168    }
169    let tok = match count {
170        1 => Some(toks[0]),
171        2 => match side_idx {
172            0 | 2 => Some(toks[0]),
173            _ => Some(toks[1]),
174        },
175        3 => match side_idx {
176            0 => Some(toks[0]),
177            2 => Some(toks[2]),
178            _ => Some(toks[1]),
179        },
180        4 => Some(toks[side_idx]),
181        _ => None,
182    }?;
183    parse_length_value(Some(String::from(tok))).and_then(|lv| resolve_length_value(lv, base))
184}
185
186/// `name(...)` の括弧内文字列を取り出す(`string_slice` lint 回避で str::get のみ使用)。
187pub(crate) fn extract_fn_args(s: &str, name: &str) -> Option<String> {
188    let needle = alloc::format!("{}(", name);
189    let pos = s.find(&needle)?;
190    let after = s.get(pos + needle.len()..)?;
191    let end = after.find(')')?;
192    after.get(..end).map(String::from)
193}
194
195/// translate の 1 引数(px / % / rem / vh / vw)を base 基準で解決。
196/// 以前は `px`/`%` の2種のみ対応で、それ以外の単位(`rem`/`vh`/`vw`等)は
197/// `t.strip_suffix("px").unwrap_or(t)`が単位付き文字列をそのまま数値
198/// パースへ渡してしまい、静かに`0`へフォールバックしていた——単に
199/// 「対応外」なのではなく、`transform: translate(1rem, 10px)`のような
200/// 式で意図した移動量が消えて要素が動かなくなる、実害のあるバグだった。
201/// `em`(要素自身の算出フォントサイズ基準)はこの関数の引数だけでは
202/// 算出フォントサイズを解決できないため対象外のまま
203/// (`font-size`の継承解決には別途プロパティ探索が必要で、誤った既定値
204/// 〔例:常に16px〕を当てはめると`rem`と区別が付かない別の壊れ方になる
205/// リスクがあり見送り)。2026-07-17 発見・実装。
206pub(crate) fn resolve_translate_len(tok: &str, base: i32) -> i32 {
207    let t = tok.trim();
208    if let Some(p) = t.strip_suffix('%') {
209        return p
210            .trim()
211            .parse::<f32>()
212            .map(|v| (v / 100.0 * base as f32) as i32)
213            .unwrap_or(0);
214    }
215    if let Some(p) = t.strip_suffix("rem") {
216        return p.trim().parse::<f32>().map(|v| (v * 16.0) as i32).unwrap_or(0);
217    }
218    if let Some(p) = t.strip_suffix("vh") {
219        let h = super::values::VIEWPORT_HINT_H.load(core::sync::atomic::Ordering::Relaxed);
220        return p
221            .trim()
222            .parse::<f32>()
223            .map(|v| (v / 100.0 * h as f32) as i32)
224            .unwrap_or(0);
225    }
226    if let Some(p) = t.strip_suffix("vw") {
227        let w = super::values::VIEWPORT_HINT_W.load(core::sync::atomic::Ordering::Relaxed);
228        return p
229            .trim()
230            .parse::<f32>()
231            .map(|v| (v / 100.0 * w as f32) as i32)
232            .unwrap_or(0);
233    }
234    let n = t.strip_suffix("px").unwrap_or(t).trim();
235    n.parse::<f32>().map(|v| v as i32).unwrap_or(0)
236}
237
238/// `transform` から translate オフセット (dx, dy) を取り出す。% は要素自身の (w,h) 基準。
239/// translate(x[,y]) / translateX(x) / translateY(y) に対応(scale/rotate は未対応・無視)。
240pub(crate) fn box_translate(b: &LayoutBox) -> (i32, i32) {
241    let t = match box_style_value(b, "transform") {
242        Some(v) => v,
243        None => return (0, 0),
244    };
245    let w = b.dimensions.content.width;
246    let h = b.dimensions.content.height;
247    if let Some(args) = extract_fn_args(&t, "translate") {
248        let mut parts = args.split(',');
249        let dx = parts
250            .next()
251            .map(|a| resolve_translate_len(a, w))
252            .unwrap_or(0);
253        let dy = parts
254            .next()
255            .map(|a| resolve_translate_len(a, h))
256            .unwrap_or(0);
257        return (dx, dy);
258    }
259    let dx = extract_fn_args(&t, "translateX")
260        .map(|a| resolve_translate_len(&a, w))
261        .unwrap_or(0);
262    let dy = extract_fn_args(&t, "translateY")
263        .map(|a| resolve_translate_len(&a, h))
264        .unwrap_or(0);
265    (dx, dy)
266}
267
268/// グリッド占有 (rowspan/colspan 解決用) のヘルパ。
269pub(crate) fn occ_is(occ: &[Vec<bool>], r: usize, c: usize) -> bool {
270    occ.get(r)
271        .and_then(|row| row.get(c))
272        .copied()
273        .unwrap_or(false)
274}
275pub(crate) fn occ_mark(occ: &mut Vec<Vec<bool>>, r: usize, c: usize) {
276    while occ.len() <= r {
277        occ.push(Vec::new());
278    }
279    let row = &mut occ[r];
280    while row.len() <= c {
281        row.push(false);
282    }
283    row[c] = true;
284}
285
286// レイアウトツリーの構築と計算