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}