Skip to main content

atmos/os_lib/layout/
multicol.rs

1//! CSS 多段組(`column-count` / `column-width` / `columns` / `column-gap`)の
2//! 列幾何計算(純粋モジュール)。
3//!
4//! ハードウェアにもグローバル状態にも依存しない算術だけなので、
5//! QEMU 抜きでホスト側から検証できる。
6//!
7//! # 仕様の要点(CSS Multi-column Layout Level 1)
8//!
9//! - `column-count` は列数の指定、`column-width` は 1 列の**最小**目安幅。
10//!   両方指定されたときは「`column-width` から求まる列数」と
11//!   `column-count` の**小さい方**を採る(`column-count` は上限として働く)。
12//! - `column-gap` の既定は `normal`=`1em` 相当。ここでは呼び出し側が
13//!   解決したピクセル値を受け取る。
14//! - 列は等幅で、`gap` は列と列の**間だけ**に入る(外側には入らない)。
15//!   よって `列幅 = (使用可能幅 - gap * (列数 - 1)) / 列数`。
16//!
17//! # 割り切り
18//!
19//! 段抜き(`column-span`)・段区切り(`break-inside`)・段罫線
20//! (`column-rule`)は扱わない。本モジュールは「何列で、各列はどの
21//! x 座標にどの幅で並ぶか」だけを決める。
22
23extern crate alloc;
24use alloc::vec::Vec;
25
26/// 多段組の指定。いずれも解決済みのピクセル値/個数。
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct MultiColSpec {
29    /// `column-count`。未指定は `None`。
30    pub count: Option<i32>,
31    /// `column-width`(1 列の最小目安幅、px)。未指定は `None`。
32    pub width: Option<i32>,
33    /// `column-gap`(px)。`normal` は呼び出し側で 1em 相当へ解決しておく。
34    pub gap: i32,
35}
36
37/// 列の位置と幅。
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct Column {
40    /// コンテンツボックス左端からの x オフセット(px)。
41    pub x: i32,
42    /// 列の幅(px)。
43    pub width: i32,
44}
45
46/// 実際に使う列数を決める(純粋関数)。
47///
48/// - どちらも未指定なら 1 列(多段組ではない)。
49/// - `count` だけなら その値(1 未満は 1 に切り上げ)。
50/// - `width` だけなら 使用可能幅から入るだけ詰める。
51/// - 両方あれば **小さい方**(`count` が上限)。
52///
53/// `avail_w` が 0 以下、または `width` が 0 以下のときは 1 列へ倒す
54/// (ゼロ除算と無限列を防ぐ)。
55pub fn resolve_column_count(spec: &MultiColSpec, avail_w: i32) -> i32 {
56    if avail_w <= 0 {
57        return 1;
58    }
59    // `column-width` から入る列数を求める。gap も 1 列分ずつ消費する。
60    let from_width = spec.width.and_then(|w| {
61        if w <= 0 {
62            return None;
63        }
64        // n 列入る条件: n*w + (n-1)*gap <= avail_w
65        //            → n <= (avail_w + gap) / (w + gap)
66        let denom = w.saturating_add(spec.gap.max(0));
67        if denom <= 0 {
68            return None;
69        }
70        let n = (avail_w.saturating_add(spec.gap.max(0))) / denom;
71        Some(n.max(1))
72    });
73    let from_count = spec.count.map(|c| c.max(1));
74
75    match (from_count, from_width) {
76        (None, None) => 1,
77        (Some(c), None) => c,
78        (None, Some(w)) => w,
79        // 仕様上 `column-count` は上限として働く。
80        (Some(c), Some(w)) => c.min(w),
81    }
82}
83
84/// 列の x 座標と幅を計算する(純粋関数)。
85///
86/// `gap` は列間にだけ入る。端数は最後の列へ寄せず、各列を等幅にしたうえで
87/// 余りぶんだけ最終列を広げる(合計が `avail_w` を超えないようにする)。
88/// 列幅が負になる(gap が広すぎる)場合は 0 幅の列を返し、パニックしない。
89pub fn layout_columns(spec: &MultiColSpec, avail_w: i32) -> Vec<Column> {
90    let n = resolve_column_count(spec, avail_w);
91    let gap = spec.gap.max(0);
92    if n <= 1 {
93        return alloc::vec![Column {
94            x: 0,
95            width: avail_w.max(0),
96        }];
97    }
98    let total_gap = gap.saturating_mul(n - 1);
99    let usable = avail_w.saturating_sub(total_gap);
100    if usable <= 0 {
101        // gap だけで埋まってしまう場合。列は作るが幅 0 にする。
102        return (0..n)
103            .map(|i| Column {
104                x: (gap.saturating_add(0)).saturating_mul(i),
105                width: 0,
106            })
107            .collect();
108    }
109    let base = usable / n;
110    let mut cols = Vec::with_capacity(n as usize);
111    let mut x = 0;
112    for i in 0..n {
113        // 端数(usable % n)は最終列で吸収し、合計が avail_w を超えないようにする。
114        let w = if i == n - 1 { usable - base * (n - 1) } else { base };
115        cols.push(Column { x, width: w });
116        x = x.saturating_add(w).saturating_add(gap);
117    }
118    cols
119}
120
121/// `columns` ショートハンドを `column-width` と `column-count` へ分解する。
122///
123/// 仕様上、値は「長さ」と「整数」が任意順で高々 1 つずつ。
124/// `auto` は「その成分は未指定」を意味する。
125/// 解決できない字句は無視する(不正値でページ全体を壊さない)。
126///
127/// 長さの解決(`px` 以外の単位)は呼び出し側の責務。ここでは
128/// **末尾が `px` か単位無しの数値**だけを長さとして受け取り、
129/// 整数だけの値は列数として扱う。
130pub fn parse_columns_shorthand(s: &str) -> (Option<i32>, Option<i32>) {
131    let mut width = None;
132    let mut count = None;
133    for tok in s.split_whitespace() {
134        let t = tok.trim();
135        if t.is_empty() || t.eq_ignore_ascii_case("auto") {
136            continue;
137        }
138        if let Some(px) = t.strip_suffix("px") {
139            if let Ok(v) = px.trim().parse::<f32>() {
140                if v.is_finite() && v > 0.0 {
141                    width = Some(v as i32);
142                }
143            }
144            continue;
145        }
146        // 単位無しの整数は列数。小数や負数は無視する。
147        if let Ok(v) = t.parse::<i32>() {
148            if v > 0 {
149                count = Some(v);
150            }
151            continue;
152        }
153        // それ以外(`2em` 等の未対応単位)は長さとして扱えないので無視する。
154    }
155    (width, count)
156}
157
158/// 子ボックスを列へ振り分けた結果。
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub struct Placement {
161    /// 何列目へ入れるか(0 起点)。
162    pub col: usize,
163    /// その列の中での上端オフセット(px)。
164    pub y: i32,
165}
166
167/// 子の高さ列を受け取り、各子をどの列のどの高さへ置くかを決める(純粋関数)。
168///
169/// 不変条件 M-6 のとおり「その時点で高さが最小の列」へ順に入れる貪欲法。
170/// 同じ高さの列が複数あるときは**左の列を優先**する(見た目の安定のため。
171/// 右から埋まると 1 件だけのときに不自然に右へ寄る)。
172///
173/// 返り値の長さは `heights` と同じ。列数が 0 のときは全て 0 列目へ倒す。
174///
175/// 計算量: **O(子数 × 列数)**。列数は高々数十なので実用上問題ない。
176pub fn assign_to_columns(heights: &[i32], n_cols: usize) -> Vec<Placement> {
177    let n = n_cols.max(1);
178    let mut bottoms = alloc::vec![0i32; n];
179    let mut out = Vec::with_capacity(heights.len());
180    for &h in heights {
181        // 最小の列を探す。`<` で比較するので同値なら先に見た(左の)列が残る。
182        let mut min_i = 0usize;
183        for i in 1..n {
184            if bottoms[i] < bottoms[min_i] {
185                min_i = i;
186            }
187        }
188        out.push(Placement {
189            col: min_i,
190            y: bottoms[min_i],
191        });
192        bottoms[min_i] = bottoms[min_i].saturating_add(h.max(0));
193    }
194    out
195}
196
197/// 振り分け後の各列の高さを返す(純粋関数)。
198///
199/// 多段組ブロック自身の高さは、この最大値になる。
200pub fn column_heights(heights: &[i32], n_cols: usize) -> Vec<i32> {
201    let n = n_cols.max(1);
202    let mut bottoms = alloc::vec![0i32; n];
203    for &h in heights {
204        let mut min_i = 0usize;
205        for i in 1..n {
206            if bottoms[i] < bottoms[min_i] {
207                min_i = i;
208            }
209        }
210        bottoms[min_i] = bottoms[min_i].saturating_add(h.max(0));
211    }
212    bottoms
213}