Skip to main content

atmos/os_lib/layout/
grid.rs

1// 分割: layout.rs より機械的に移動(2026-07-16 リファクタ フェーズ4)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5/// `grid-template-columns` を解析して列幅 Vec を返す。
6/// `repeat(N, W)` / `repeat(auto-fit, minmax(Npx, 1fr))` / `1fr 2fr` / 固定 px に対応。
7pub(crate) fn parse_grid_template_columns(s: &str, avail_w: i32) -> Vec<i32> {
8    let s = s.trim();
9    if s.is_empty() {
10        return alloc::vec![avail_w];
11    }
12    // `repeat(N, W)` or `repeat(auto-fit, minmax(min_px, 1fr))`
13    if let Some(inner) = s.strip_prefix("repeat(").and_then(|t| t.strip_suffix(')')) {
14        if let Some((count_str, rest)) = inner.split_once(',') {
15            let count_s = count_str.trim();
16            let col_def = rest.trim();
17            // minmax(Xpx, 1fr) → auto-fit では コンテナ幅から min で列数を決める。
18            let min_w = if let Some(mm) = col_def
19                .strip_prefix("minmax(")
20                .and_then(|t| t.strip_suffix(')'))
21            {
22                mm.split_once(',')
23                    .and_then(|(a, _)| resolve_track_tok_opt(a.trim(), avail_w))
24                    .unwrap_or(200)
25            } else {
26                parse_px_like(col_def).unwrap_or(avail_w)
27            };
28            let n: usize = if count_s == "auto-fit" || count_s == "auto-fill" {
29                (libm::floorf(avail_w as f32 / min_w.max(1) as f32) as usize).max(1)
30            } else {
31                count_s.parse::<usize>().unwrap_or(1).max(1)
32            };
33            // 固定 px 列定義(`repeat(3,100px)`)はその幅を尊重。fr/minmax/% は均等割り。
34            let is_fixed_px = !col_def.ends_with("fr")
35                && !col_def.starts_with("minmax(")
36                && !col_def.ends_with('%')
37                && parse_px_like(col_def).is_some();
38            let col_w = if is_fixed_px {
39                parse_px_like(col_def).unwrap_or(1).max(1)
40            } else {
41                (avail_w / n as i32).max(1)
42            };
43            return (0..n).map(|_| col_w).collect();
44        }
45    }
46    // スペース区切りのトークン(1fr / 2fr / 200px / % など)
47    let tokens: alloc::vec::Vec<&str> = s.split_whitespace().collect();
48    if tokens.is_empty() {
49        return alloc::vec![avail_w];
50    }
51    // `%` トラック(例: `50% 1fr`)を avail_w 基準で px へ解決する。`parse_px_like` は
52    // px/vh/vw/rem/em のみ対応し `%` を扱わないため(`repeat()`/`minmax()` 経由の
53    // 固定px判定でも同様に `%` は別扱いされている、既存の `is_fixed_px` 参照)、
54    // ここで専用に解決する。
55    fn resolve_track_tok_opt(tok: &str, avail_w: i32) -> Option<i32> {
56        if let Some(pct) = tok.strip_suffix('%') {
57            return pct
58                .trim()
59                .parse::<f32>()
60                .ok()
61                .map(|p| round_f32_to_i32(avail_w as f32 * p / 100.0));
62        }
63        parse_px_like(tok)
64    }
65    fn resolve_track_tok(tok: &str, avail_w: i32) -> i32 {
66        resolve_track_tok_opt(tok, avail_w).unwrap_or(0)
67    }
68    // fr 合計と固定幅を分けて残り幅を分配。
69    let mut fixed_total = 0i32;
70    let mut fr_total = 0.0f32;
71    for tok in &tokens {
72        if let Some(fr) = tok.strip_suffix("fr") {
73            fr_total += fr.trim().parse::<f32>().unwrap_or(1.0);
74        } else {
75            fixed_total += resolve_track_tok(tok, avail_w);
76        }
77    }
78    let remaining = (avail_w - fixed_total).max(0);
79    tokens
80        .iter()
81        .map(|tok| {
82            if let Some(fr) = tok.strip_suffix("fr") {
83                let f = fr.trim().parse::<f32>().unwrap_or(1.0);
84                if fr_total > 0.0 {
85                    round_f32_to_i32(remaining as f32 * f / fr_total)
86                } else {
87                    remaining
88                }
89            } else {
90                resolve_track_tok(tok, avail_w)
91            }
92        })
93        .collect()
94}
95
96/// グリッドトラック(行/列の 1 トラック)のサイズ種別。
97#[derive(Clone, Copy)]
98pub(crate) enum GridTrack {
99    Px(i32),
100    Fr(f32),
101    Auto,
102    /// minmax(min_px, max): max が fr なら伸長可能だが最低 min_px を保証。
103    /// max が auto/px のときは Fr(0) 相当で扱い、行高さは内容 or max_px と min_px の間。
104    MinMax {
105        min_px: i32,
106        max_fr: f32,
107        max_px: i32,
108        max_is_fr: bool,
109        max_is_auto: bool,
110    },
111}
112
113/// トップレベル(括弧の外)の空白でトークン分割する。`minmax(a, b)` / `repeat(n, x)`
114/// の括弧内の空白では分割しない。`string_slice` lint 回避のため char_indices + get を使う。
115pub(crate) fn split_top_level_ws(s: &str) -> Vec<&str> {
116    let mut out = Vec::new();
117    let mut depth = 0i32;
118    let mut start = 0usize;
119    let mut in_tok = false;
120    for (i, ch) in s.char_indices() {
121        match ch {
122            '(' => depth += 1,
123            ')' => depth -= 1,
124            c if c.is_whitespace() && depth == 0 => {
125                if in_tok {
126                    if let Some(t) = s.get(start..i) {
127                        out.push(t);
128                    }
129                    in_tok = false;
130                }
131                continue;
132            }
133            _ => {}
134        }
135        if !in_tok {
136            start = i;
137            in_tok = true;
138        }
139    }
140    if in_tok {
141        if let Some(t) = s.get(start..) {
142            out.push(t);
143        }
144    }
145    out
146}
147
148/// 単一トラック定義(`100px` / `2fr` / `auto` / `50%` / `minmax(min,max)`)を解釈。
149/// base は % 解決と auto 折衝の基準サイズ。
150pub(crate) fn parse_one_grid_track(tok: &str, base: i32) -> GridTrack {
151    let tok = tok.trim();
152    if tok.is_empty() || tok.eq_ignore_ascii_case("auto") {
153        return GridTrack::Auto;
154    }
155    if let Some(fr) = tok.strip_suffix("fr") {
156        return GridTrack::Fr(fr.trim().parse::<f32>().unwrap_or(1.0).max(0.0));
157    }
158    if let Some(mm) = tok
159        .strip_prefix("minmax(")
160        .and_then(|t| t.strip_suffix(')'))
161    {
162        // minmax(min, max): min を下限 px、max を上限(fr/px/auto)として保持。
163        if let Some((a, b)) = mm.split_once(',') {
164            let min_px = match parse_length_value(Some(a.trim())) {
165                Some(LengthValue::Px(px)) => px.max(0),
166                Some(LengthValue::Percent(p)) => round_f32_to_i32(base as f32 * p).max(0),
167                _ => 0, // auto/min-content 等は 0 下限扱い
168            };
169            let bt = b.trim();
170            if let Some(fr) = bt.strip_suffix("fr") {
171                return GridTrack::MinMax {
172                    min_px,
173                    max_fr: fr.trim().parse::<f32>().unwrap_or(1.0).max(0.0),
174                    max_px: 0,
175                    max_is_fr: true,
176                    max_is_auto: false,
177                };
178            }
179            let (max_px, max_is_auto) = match parse_length_value(Some(bt)) {
180                Some(LengthValue::Px(px)) => (px.max(0), false),
181                Some(LengthValue::Percent(p)) => (round_f32_to_i32(base as f32 * p).max(0), false),
182                _ => (0, true), // auto/max-content
183            };
184            return GridTrack::MinMax {
185                min_px,
186                max_fr: 0.0,
187                max_px,
188                max_is_fr: false,
189                max_is_auto,
190            };
191        }
192    }
193    match parse_length_value(Some(tok)) {
194        Some(LengthValue::Px(px)) => GridTrack::Px(px),
195        Some(LengthValue::Percent(p)) => GridTrack::Px(round_f32_to_i32(base as f32 * p)),
196        _ => GridTrack::Auto,
197    }
198}
199
200/// `grid-template-rows` / `grid-template-columns` を GridTrack のリストへ展開。
201/// `repeat(N, def)` / `repeat(auto-fit|auto-fill, def)` を展開する。base は %/auto-fit 用。
202pub(crate) fn parse_grid_tracks(s: &str, base: i32) -> Vec<GridTrack> {
203    let s = s.trim();
204    if s.is_empty() {
205        return Vec::new();
206    }
207    let mut out = Vec::new();
208    for tok in split_top_level_ws(s) {
209        if let Some(inner) = tok
210            .strip_prefix("repeat(")
211            .and_then(|t| t.strip_suffix(')'))
212        {
213            if let Some((count_str, rest)) = inner.split_once(',') {
214                let count_s = count_str.trim();
215                let track = parse_one_grid_track(rest.trim(), base);
216                let n: usize = if count_s == "auto-fit" || count_s == "auto-fill" {
217                    let min_w = match track {
218                        GridTrack::Px(px) => px.max(1),
219                        GridTrack::MinMax { min_px, .. } => min_px.max(1),
220                        _ => 1,
221                    };
222                    (libm::floorf(base as f32 / min_w as f32) as usize).max(1)
223                } else {
224                    count_s.parse::<usize>().unwrap_or(1).max(1)
225                };
226                for _ in 0..n {
227                    out.push(track);
228                }
229                continue;
230            }
231        }
232        out.push(parse_one_grid_track(tok, base));
233    }
234    out
235}
236
237/// グリッドアイテムの 1 軸の配置を解決する。
238/// `short`(grid-row / grid-column のショートハンド `a / b`)を優先し、無ければ
239/// longhand(`*-start` / `*-end`)を読む。戻り値は (明示開始行0始まり, スパン)。
240/// 明示開始が無い(auto)の場合は None。
241pub(crate) fn grid_axis_placement(
242    b: &LayoutBox,
243    short: &str,
244    start_p: &str,
245    end_p: &str,
246) -> (Option<usize>, usize) {
247    let (sv, ev) = if let Some(v) = box_style_value(b, short) {
248        if let Some((a, c)) = v.split_once('/') {
249            (Some(String::from(a.trim())), Some(String::from(c.trim())))
250        } else {
251            (Some(String::from(v.trim())), None)
252        }
253    } else {
254        (
255            box_style_value(b, start_p).map(|s| String::from(s.trim())),
256            box_style_value(b, end_p).map(|s| String::from(s.trim())),
257        )
258    };
259    let mut start: Option<usize> = None;
260    let mut span: usize = 1;
261    if let Some(sv) = &sv {
262        if let Some(n) = sv.strip_prefix("span ") {
263            span = n.trim().parse::<usize>().unwrap_or(1).max(1);
264        } else if let Ok(line) = sv.parse::<i32>() {
265            if line >= 1 {
266                start = Some((line - 1) as usize);
267            }
268        }
269    }
270    if let Some(ev) = &ev {
271        if let Some(n) = ev.strip_prefix("span ") {
272            span = n.trim().parse::<usize>().unwrap_or(1).max(1);
273        } else if let Ok(eline) = ev.parse::<i32>() {
274            if let Some(s0) = start {
275                let e0 = (eline - 1).max(0) as usize;
276                if e0 > s0 {
277                    span = e0 - s0;
278                }
279            }
280        }
281    }
282    (start, span)
283}
284
285/// `grid-template-areas` を解析する。各文字列リテラル(クォートされた1行)が1グリッド行、
286/// 空白区切りのトークンがその行の各列のセル名になる。`.` は未使用セル(名前なし)扱い。
287/// 例: `"header header" "sidebar content"` → [["header","header"],["sidebar","content"]]
288pub(crate) fn parse_grid_template_areas(s: &str) -> Vec<Vec<String>> {
289    let mut rows = Vec::new();
290    let mut chars = s.chars().peekable();
291    while let Some(c) = chars.next() {
292        if c == '"' || c == '\'' {
293            let quote = c;
294            let mut row_str = String::new();
295            for c2 in chars.by_ref() {
296                if c2 == quote {
297                    break;
298                }
299                row_str.push(c2);
300            }
301            let row: Vec<String> = row_str
302                .split_whitespace()
303                .map(String::from)
304                .collect();
305            if !row.is_empty() {
306                rows.push(row);
307            }
308        }
309    }
310    rows
311}
312
313/// `template_areas` 内で `name` という名前のセルが占める矩形のバウンディングボックスを返す。
314/// (row_start, col_start, row_span, col_span)(いずれも0始まり)。`.` は名前なしセルなので対象外。
315pub(crate) fn find_grid_area(
316    template_areas: &[Vec<String>],
317    name: &str,
318) -> Option<(usize, usize, usize, usize)> {
319    if name == "." {
320        return None;
321    }
322    let mut min_r = usize::MAX;
323    let mut max_r = 0usize;
324    let mut min_c = usize::MAX;
325    let mut max_c = 0usize;
326    let mut found = false;
327    for (r, row) in template_areas.iter().enumerate() {
328        for (c, cell) in row.iter().enumerate() {
329            if cell == name {
330                found = true;
331                min_r = min_r.min(r);
332                max_r = max_r.max(r);
333                min_c = min_c.min(c);
334                max_c = max_c.max(c);
335            }
336        }
337    }
338    if !found {
339        return None;
340    }
341    Some((min_r, min_c, max_r - min_r + 1, max_c - min_c + 1))
342}
343
344/// occupancy 行を必要分まで確保(n_cols 幅の false 行を追加)。
345pub(crate) fn grid_ensure_row(occ: &mut Vec<Vec<bool>>, r: usize, n_cols: usize) {
346    while occ.len() <= r {
347        occ.push(alloc::vec![false; n_cols]);
348    }
349}
350
351/// `grid-auto-flow: column` が明示列数を超えて新しい列を作った際、既存の全行の
352/// occupancy 幅を min_cols まで広げる(新規に作る行は grid_ensure_row 側で
353/// 正しい幅で作られるため対象外。既存行だけ広げれば足りる)。
354pub(crate) fn grid_ensure_col(occ: &mut [Vec<bool>], min_cols: usize) {
355    for row in occ.iter_mut() {
356        while row.len() < min_cols {
357            row.push(false);
358        }
359    }
360}
361
362/// (r,c) から row_span×col_span の矩形が全て空いているか。
363pub(crate) fn grid_cells_free(
364    occ: &[Vec<bool>],
365    r: usize,
366    c: usize,
367    row_span: usize,
368    col_span: usize,
369    n_cols: usize,
370) -> bool {
371    if c + col_span > n_cols {
372        return false;
373    }
374    for rr in r..r + row_span {
375        let Some(row) = occ.get(rr) else { continue };
376        for cc in c..c + col_span {
377            if row.get(cc).copied().unwrap_or(false) {
378                return false;
379            }
380        }
381    }
382    true
383}
384
385/// (r,c) から row_span×col_span を占有済みにする。
386pub(crate) fn grid_mark(
387    occ: &mut [Vec<bool>],
388    r: usize,
389    c: usize,
390    row_span: usize,
391    col_span: usize,
392    n_cols: usize,
393) {
394    for rr in r..r + row_span {
395        let Some(row) = occ.get_mut(rr) else { continue };
396        for cc in c..(c + col_span).min(n_cols) {
397            if let Some(cell) = row.get_mut(cc) {
398                *cell = true;
399            }
400        }
401    }
402}
403
404/// 列インデックス col の左端 x オフセット(列幅 + col_gap の累積)。
405pub(crate) fn grid_col_x(col_widths: &[i32], col_gap: i32, col: usize) -> i32 {
406    let mut x = 0i32;
407    for (i, w) in col_widths.iter().enumerate() {
408        if i >= col {
409            break;
410        }
411        x += w + col_gap;
412    }
413    x
414}
415
416/// col から col_span 列ぶんの合計幅(内側の col_gap を含む)。
417pub(crate) fn grid_span_width(col_widths: &[i32], col_gap: i32, col: usize, col_span: usize) -> i32 {
418    let n = col_widths.len();
419    if n == 0 {
420        return 1;
421    }
422    let end = (col + col_span).min(n);
423    let start = col.min(n.saturating_sub(1));
424    let mut w = 0i32;
425    for item in col_widths.iter().take(end).skip(start) {
426        w += *item;
427    }
428    w += (end.saturating_sub(start).saturating_sub(1) as i32) * col_gap;
429    w.max(1)
430}
431