Skip to main content

atmos/os_lib/layout/
flex_min.rs

1//! フレックスアイテムの縮小配分(自動最小サイズを尊重する)。
2//!
3//! 仕様・典拠・不変条件 F-1〜F-5 は `spec/flex_min_size.md` を参照。
4//! 典拠: CSS Flexible Box Layout Level 1 §4.5 / §9.7
5//! (https://www.w3.org/TR/css-flexbox-1/#algo-main-item)
6//!
7//! ## なぜ必要か
8//! 従来の実装は超過分を shrink 係数で按分したあと `.max(0)` で 0 までしか
9//! 止めておらず、**自動最小サイズを一切見ていなかった**。その結果
10//! `.hero-content` が 240px まで縮み、14 文字の見出しが 3 行に過剰折り返しされ、
11//! サブテキストと重なっていた。
12//!
13//! ## 近似であることの明示
14//! 原典 §9.7 は violation の符号で分岐する反復アルゴリズムだが、本実装は
15//! 「下限に達したアイテムを凍結して残余を再配分する」ループに簡略化している。
16//! 自動最小サイズも min-content 幅で代用する近似(`spec/flex_min_size.md` 参照)。
17//!
18//! グローバル状態にもハードウェアにも依存しない純粋ロジックのみで構成する。
19
20extern crate alloc;
21
22use alloc::vec::Vec;
23
24/// 縮小対象のアイテム 1 つ分の入力。
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct ShrinkItem {
27    /// 縮小前の主軸サイズ。
28    pub base: i32,
29    /// 下限。自動最小サイズと `min-width` 指定の大きい方。
30    pub min: i32,
31    /// `flex-shrink` 係数。0 なら縮まない。
32    pub shrink: i32,
33}
34
35/// 配分の引数が不正だった場合のエラー。
36#[derive(Debug, PartialEq, Eq, Clone, Copy)]
37pub enum FlexError {
38    /// 主軸サイズが負。
39    NegativeBase(i32),
40    /// 下限が負。
41    NegativeMin(i32),
42    /// `flex-shrink` 係数が負。
43    NegativeShrink(i32),
44    /// 吸収すべき超過分が負(呼び出し側の計算違い)。
45    NegativeExcess(i32),
46}
47
48/// 超過分 `excess` を各アイテムへ按分して縮小し、最終サイズを返す。
49///
50/// 下限(`min`)に達したアイテムは**凍結**し、残りの超過分は
51/// まだ縮める余地のあるアイテムへ再配分する(不変条件 F-3)。
52/// 全アイテムが下限に達しても超過が残る場合は、**それ以上縮めない**。
53/// コンテナからはみ出すのが正しい挙動(F-4)。
54///
55/// 引数が不正(負値)なときは黙って戻らず、エラーログを出して `Err` を返す。
56/// 呼び出し側は縮小を行わない(=base のまま)方向へフォールバックすること。
57/// 計算量: **O(I²)** 最悪 — I はフレックスアイテム数。
58///
59/// 自動最小サイズ(`min-width:auto`)で下限に張り付いたアイテムを
60/// 凍結して再分配するため、**1 回の反復で最低 1 個は凍結される**。
61/// 反復は最大 I 回、各回 O(I)。実用上のアイテム数(数個〜数十)では問題ない。
62pub fn distribute_shrink(items: &[ShrinkItem], excess: i32) -> Result<Vec<i32>, FlexError> {
63    if excess < 0 {
64        crate::error!("[FLEX][SHRINK] 超過分が負です: {}", excess);
65        return Err(FlexError::NegativeExcess(excess));
66    }
67    for it in items {
68        if it.base < 0 {
69            crate::error!("[FLEX][SHRINK] 主軸サイズが負です: {}", it.base);
70            return Err(FlexError::NegativeBase(it.base));
71        }
72        if it.min < 0 {
73            crate::error!("[FLEX][SHRINK] 下限が負です: {}", it.min);
74            return Err(FlexError::NegativeMin(it.min));
75        }
76        if it.shrink < 0 {
77            crate::error!("[FLEX][SHRINK] flex-shrink が負です: {}", it.shrink);
78            return Err(FlexError::NegativeShrink(it.shrink));
79        }
80    }
81
82    let n = items.len();
83    let mut sizes: Vec<i32> = items.iter().map(|i| i.base).collect();
84    let mut frozen: Vec<bool> = items
85        .iter()
86        // shrink=0 のアイテムは最初から縮まない。既に下限以下のものも凍結。
87        .map(|i| i.shrink == 0 || i.base <= i.min)
88        .collect();
89    let mut remaining = excess;
90
91    // 凍結が起きるたびに再配分するので、最大でもアイテム数回で収束する。
92    for _ in 0..=n {
93        if remaining <= 0 {
94            break;
95        }
96        let total_shrink: i64 = items
97            .iter()
98            .zip(frozen.iter())
99            .filter(|(_, f)| !**f)
100            .map(|(i, _)| i.shrink as i64)
101            .sum();
102        if total_shrink == 0 {
103            // 縮める余地が無い。F-4: これ以上縮めずコンテナからはみ出させる。
104            break;
105        }
106
107        let mut violated = false;
108        let mut consumed = 0i32;
109        for idx in 0..n {
110            if frozen[idx] {
111                continue;
112            }
113            let cut = ((remaining as i64 * items[idx].shrink as i64) / total_shrink) as i32;
114            let target = sizes[idx] - cut;
115            if target <= items[idx].min {
116                // F-1/F-2: 下限を下回らせない。凍結して残余を他へ回す。
117                consumed += sizes[idx] - items[idx].min;
118                sizes[idx] = items[idx].min;
119                frozen[idx] = true;
120                violated = true;
121            } else {
122                sizes[idx] = target;
123                consumed += cut;
124            }
125        }
126        remaining -= consumed;
127        if !violated {
128            break;
129        }
130    }
131
132    // F-5: 負のサイズは決して生じない。
133    for s in sizes.iter_mut() {
134        if *s < 0 {
135            *s = 0;
136        }
137    }
138    Ok(sizes)
139}