Skip to main content

atmos/os_lib/layout/
types.rs

1// 分割: layout.rs より機械的に移動(2026-07-16 リファクタ フェーズ4)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5pub(crate) static IMAGE_SIZE_CACHE: Mutex<Option<BTreeMap<String, (i32, i32)>>> = Mutex::new(None);
6
7/// 【2026-08-05】ネットワークから取得した画像の自然サイズを登録する。
8///
9/// `<img>` に `width`/`height` が無い場合、レイアウトは画像の自然サイズを
10/// 使うべきだが、`values.rs` はローカルファイルシステム(`src` が `/` 始まり)
11/// しか見ていなかった。ネットワーク画像は**既定の 400×200** に落ちるため、
12/// `App_Store_Badge_US-UK_135x40.svg`(本来 135×40)が
13/// 画面外へはみ出すほど大きく描かれていた。
14///
15/// デコード完了時にここへ登録し、次のレイアウトで拾わせる。
16pub fn register_image_natural_size(src: &str, w: i32, h: i32) {
17    let mut cache = IMAGE_SIZE_CACHE.lock();
18    if cache.is_none() {
19        *cache = Some(BTreeMap::new());
20    }
21    if let Some(ref mut c) = *cache {
22        c.insert(alloc::string::String::from(src), (w, h));
23    }
24}
25
26#[derive(Debug, Clone, Default)]
27pub struct Rect {
28    pub x: i32,
29    pub y: i32,
30    pub width: i32,
31    pub height: i32,
32}
33
34#[derive(Debug, Clone, Default)]
35pub struct EdgeSizes {
36    pub left: i32,
37    pub right: i32,
38    pub top: i32,
39    pub bottom: i32,
40}
41
42#[derive(Debug, Clone, Default)]
43pub struct Dimensions {
44    pub content: Rect,
45    pub padding: EdgeSizes,
46    pub border: EdgeSizes,
47    pub margin: EdgeSizes,
48}
49
50impl Dimensions {
51    pub fn margin_box(&self) -> Rect {
52        Rect {
53            x: self.content.x - self.padding.left - self.border.left - self.margin.left,
54            y: self.content.y - self.padding.top - self.border.top - self.margin.top,
55            width: self.content.width
56                + self.padding.left
57                + self.padding.right
58                + self.border.left
59                + self.border.right
60                + self.margin.left
61                + self.margin.right,
62            height: self.content.height
63                + self.padding.top
64                + self.padding.bottom
65                + self.border.top
66                + self.border.bottom
67                + self.margin.top
68                + self.margin.bottom,
69        }
70    }
71}
72
73pub struct LayoutBox<'a> {
74    pub dimensions: Dimensions,
75    pub box_type: BoxType<'a>,
76    pub children: Vec<LayoutBox<'a>>,
77}
78
79pub enum BoxType<'a> {
80    BlockNode(&'a StyledNode<'a>),
81    InlineNode(&'a StyledNode<'a>),
82    InlineBlockNode(&'a StyledNode<'a>),
83    FlexNode(&'a StyledNode<'a>),
84    GridNode(&'a StyledNode<'a>),
85    TableNode(&'a StyledNode<'a>),
86    TableRowGroupNode(&'a StyledNode<'a>),
87    TableRowNode(&'a StyledNode<'a>),
88    TableCellNode(&'a StyledNode<'a>),
89    AnonymousBlock,
90}
91
92/// タグ名から UA デフォルトの display を返す (build_layout_tree 用)。
93/// CSS で display が明示されていない場合のフォールバック。
94pub(crate) fn default_display_for(tag_name: &str) -> &'static str {
95    match tag_name {
96        "head" | "title" | "meta" | "link" | "style" | "script" => "none",
97        "html" | "body" | "div" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol"
98        | "li" | "form" | "center" | "header" | "footer" | "main" | "section" | "nav" | "hr"
99        | "caption" | "details" | "summary" => "block",
100        "table" => "table",
101        "thead" | "tbody" | "tfoot" => "table-row-group",
102        "tr" => "table-row",
103        "td" | "th" => "table-cell",
104        _ => "inline",
105    }
106}
107
108/// セルの colspan / rowspan を読む (HTML 属性)。1 未満や巨大値はクランプ。
109pub(crate) fn cell_spans(cell: &LayoutBox) -> (usize, usize) {
110    if let BoxType::TableCellNode(styled) = &cell.box_type {
111        if let NodeType::Element { attributes, .. } = &styled.node.node_type {
112            let cs = attributes
113                .get("colspan")
114                .and_then(|v| v.trim().parse::<usize>().ok())
115                .unwrap_or(1)
116                .clamp(1, 256);
117            let rs = attributes
118                .get("rowspan")
119                .and_then(|v| v.trim().parse::<usize>().ok())
120                .unwrap_or(1)
121                .clamp(1, 1024);
122            return (cs, rs);
123        }
124    }
125    (1, 1)
126}
127
128/// LayoutBox が表す要素のタグ名 (text ノードや AnonymousBlock は None)。
129pub(crate) fn box_tag_name<'a>(b: &LayoutBox<'a>) -> Option<&'a str> {
130    let styled = match &b.box_type {
131        BoxType::BlockNode(s)
132        | BoxType::InlineNode(s)
133        | BoxType::InlineBlockNode(s)
134        | BoxType::FlexNode(s)
135        | BoxType::GridNode(s)
136        | BoxType::TableNode(s)
137        | BoxType::TableRowGroupNode(s)
138        | BoxType::TableRowNode(s)
139        | BoxType::TableCellNode(s) => *s,
140        BoxType::AnonymousBlock => return None,
141    };
142    match &styled.node.node_type {
143        NodeType::Element { tag_name, .. } => Some(tag_name.as_str()),
144        NodeType::Text(_) => None,
145    }
146}
147
148/// LayoutBox が表す要素の CSS プロパティ値(flex の子サイズ判定用)。
149pub(crate) fn box_style_value<'a>(b: &LayoutBox<'a>, prop: &str) -> Option<String> {
150    let styled = match &b.box_type {
151        BoxType::BlockNode(s)
152        | BoxType::InlineNode(s)
153        | BoxType::InlineBlockNode(s)
154        | BoxType::FlexNode(s)
155        | BoxType::GridNode(s)
156        | BoxType::TableNode(s)
157        | BoxType::TableRowGroupNode(s)
158        | BoxType::TableRowNode(s)
159        | BoxType::TableCellNode(s) => *s,
160        BoxType::AnonymousBlock => return None,
161    };
162    styled.value(prop)
163}
164
165/// 指定プロパティが明示指定されているか(auto/空は未指定扱い)。
166pub(crate) fn box_has_explicit(b: &LayoutBox, prop: &str) -> bool {
167    box_style_value(b, prop)
168        .map(|v| {
169            let t = v.trim().to_lowercase();
170            !t.is_empty() && t != "auto"
171        })
172        .unwrap_or(false)
173}
174
175/// flex アイテムの flex-grow 係数(`flex-grow` 優先、無ければ `flex` ショートハンド先頭)。
176pub(crate) fn box_flex_grow(b: &LayoutBox) -> i32 {
177    if let Some(g) = box_style_value(b, "flex-grow") {
178        return g.trim().parse::<i32>().unwrap_or(0).max(0);
179    }
180    if let Some(f) = box_style_value(b, "flex") {
181        let t = f.trim().to_lowercase();
182        if t == "none" || t == "initial" {
183            return 0;
184        }
185        if t == "auto" {
186            return 1; // flex:auto = 1 1 auto
187        }
188        if let Some(first) = t.split_whitespace().next() {
189            return first.parse::<i32>().unwrap_or(0).max(0);
190        }
191    }
192    0
193}
194
195/// flex アイテムの flex-shrink 係数(`flex-shrink` 優先、無ければ `flex` ショートハンドの
196/// 2 番目、無指定は 1 = 既定で縮小可)。`flex:none` は 0、`flex:N`(単一数値)は 1。
197/// トークンが `flex` ショートハンドの basis に使える長さ値(px/%/em/rem/vh/vw)かどうかを
198/// 判定する。以前は `px`/`%`/`em` の3種のみ対応しており、`flex: 1 40vw` のような
199/// vw/vh/rem 単位の basis が length として認識されず、`box_flex_shrink` では誤って
200/// shrink の数値パースに回されて(失敗し既定値1にフォールバック)、`box_flex_basis` では
201/// 該当トークンが見つからず basis が丸ごと失われる、という実害のあるバグがあった
202/// (`transition`/`animation` の時間値誤分類と同種の「対応単位の一部だけを見る」バグ)。
203/// 末尾を剥がした残りが実際に数値としてパースできることまで確認する。
204pub(crate) fn is_flex_length_token(tok: &str) -> bool {
205    for suffix in ["px", "%", "rem", "em", "vh", "vw"] {
206        if let Some(n) = tok.strip_suffix(suffix) {
207            if n.trim().parse::<f32>().is_ok() {
208                return true;
209            }
210        }
211    }
212    false
213}
214
215pub(crate) fn box_flex_shrink(b: &LayoutBox) -> i32 {
216    if let Some(s) = box_style_value(b, "flex-shrink") {
217        return s.trim().parse::<i32>().unwrap_or(1).max(0);
218    }
219    if let Some(f) = box_style_value(b, "flex") {
220        let t = f.trim().to_lowercase();
221        if t == "none" {
222            return 0; // flex:none = 0 0 auto
223        }
224        if t == "initial" {
225            return 1; // flex:initial = 0 1 auto
226        }
227        if t == "auto" {
228            return 1; // flex:auto = 1 1 auto
229        }
230        // `flex: <grow> <shrink> <basis>` の 2 番目があれば採用。
231        let parts: Vec<&str> = t.split_whitespace().collect();
232        if let Some(second) = parts.get(1) {
233            // 2 番目が単位付き(basis)なら shrink 省略 → 既定 1。
234            if !is_flex_length_token(second) && *second != "auto" {
235                return second.parse::<i32>().unwrap_or(1).max(0);
236            }
237        }
238        // `flex: N`(単一数値)は 1 1 0 相当 → shrink=1。
239        return 1;
240    }
241    1
242}
243
244/// flex/grid アイテムの `order`(描画順の並び替え)。未指定/不正は 0。負値可。
245pub(crate) fn box_order(b: &LayoutBox) -> i32 {
246    box_style_value(b, "order")
247        .and_then(|v| v.trim().parse::<i32>().ok())
248        .unwrap_or(0)
249}
250
251/// flex アイテムの flex-basis(主軸初期サイズ)。`flex-basis` 優先、無ければ `flex`
252/// ショートハンドの長さトークン(px/%)。`auto`/`content`/未指定は None(width/height に従う)。
253/// `main_size` は % 解決の基準(コンテナ主軸長)。
254pub(crate) fn box_flex_basis(b: &LayoutBox, main_size: i32) -> Option<i32> {
255    // 明示 flex-basis を最優先。
256    if let Some(v) = box_style_value(b, "flex-basis") {
257        let t = v.trim().to_lowercase();
258        if t.is_empty() || t == "auto" || t == "content" {
259            return None;
260        }
261        return resolve_basis_len(&t, main_size);
262    }
263    // flex ショートハンドの第3トークン(長さ)から basis を拾う。
264    if let Some(f) = box_style_value(b, "flex") {
265        let t = f.trim().to_lowercase();
266        if t == "none" || t == "auto" || t == "initial" {
267            return None; // basis=auto 相当
268        }
269        // `flex: <grow> <shrink>? <basis>?` の末尾トークンが長さなら basis。
270        for tok in t.split_whitespace() {
271            if is_flex_length_token(tok) {
272                return resolve_basis_len(tok, main_size);
273            }
274        }
275        // `flex: N`(単一数値)は basis=0。
276        if t.split_whitespace().count() == 1 && t.parse::<i32>().is_ok() {
277            return Some(0);
278        }
279    }
280    None
281}
282
283/// flex-basis のトークン("40px" / "25%" / "2em")を px へ解決する。
284pub(crate) fn resolve_basis_len(tok: &str, main_size: i32) -> Option<i32> {
285    if let Some(p) = tok.strip_suffix('%') {
286        return p
287            .trim()
288            .parse::<f32>()
289            .ok()
290            .map(|pct| round_f32_to_i32(main_size as f32 * pct / 100.0).max(0));
291    }
292    parse_px_like(tok).map(|v| v.max(0))
293}
294
295/// flex アイテムの align-self(個別交差軸整列)。未指定/auto なら None(コンテナの align-items に従う)。
296pub(crate) fn box_align_self(b: &LayoutBox) -> Option<String> {
297    box_style_value(b, "align-self").and_then(|v| {
298        let t = v.trim().to_lowercase();
299        if t.is_empty() || t == "auto" {
300            None
301        } else {
302            Some(t)
303        }
304    })
305}
306
307/// grid アイテムの整列値を解決する。`self_prop`(justify-self/align-self)が
308/// auto/未指定なら、コンテナの `items_default`(justify-items/align-items の値)へ委譲。
309/// 返り値を正規化: "start"/"end"/"center"/"stretch"。`flex-start`/`flex-end`/`self-start`/
310/// `self-end` も start/end に正規化する。既定は stretch。
311pub(crate) fn grid_item_align(b: &LayoutBox, self_prop: &str, items_default: &str) -> &'static str {
312    let raw = match box_style_value(b, self_prop) {
313        Some(v) => {
314            let t = v.trim().to_lowercase();
315            if t.is_empty() || t == "auto" {
316                String::from(items_default)
317            } else {
318                t
319            }
320        }
321        None => String::from(items_default),
322    };
323    match raw.as_str() {
324        "center" => "center",
325        "end" | "flex-end" | "self-end" | "right" => "end",
326        "start" | "flex-start" | "self-start" | "left" => "start",
327        _ => "stretch",
328    }
329}
330