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
73// Clone: inline-flex の寸法計測で試し組みするため(2026-09-26)。
74#[derive(Clone)]
75pub struct LayoutBox<'a> {
76    pub dimensions: Dimensions,
77    pub box_type: BoxType<'a>,
78    pub children: Vec<LayoutBox<'a>>,
79}
80
81#[derive(Clone, Copy)]
82pub enum BoxType<'a> {
83    BlockNode(&'a StyledNode<'a>),
84    InlineNode(&'a StyledNode<'a>),
85    InlineBlockNode(&'a StyledNode<'a>),
86    FlexNode(&'a StyledNode<'a>),
87    GridNode(&'a StyledNode<'a>),
88    TableNode(&'a StyledNode<'a>),
89    TableRowGroupNode(&'a StyledNode<'a>),
90    TableRowNode(&'a StyledNode<'a>),
91    TableCellNode(&'a StyledNode<'a>),
92    AnonymousBlock,
93}
94
95/// タグ名から UA デフォルトの display を返す (build_layout_tree 用)。
96/// CSS で display が明示されていない場合のフォールバック。
97pub(crate) fn default_display_for(tag_name: &str) -> &'static str {
98    match tag_name {
99        "head" | "title" | "meta" | "link" | "style" | "script" => "none",
100        "html" | "body" | "div" | "p" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "ul" | "ol"
101        | "li" | "form" | "center" | "header" | "footer" | "main" | "section" | "nav" | "hr"
102        | "caption" | "details" | "summary" => "block",
103        "table" => "table",
104        "thead" | "tbody" | "tfoot" => "table-row-group",
105        "tr" => "table-row",
106        "td" | "th" => "table-cell",
107        _ => "inline",
108    }
109}
110
111/// セルの colspan / rowspan を読む (HTML 属性)。1 未満や巨大値はクランプ。
112pub(crate) fn cell_spans(cell: &LayoutBox) -> (usize, usize) {
113    if let BoxType::TableCellNode(styled) = &cell.box_type {
114        if let NodeType::Element { attributes, .. } = &styled.node.node_type {
115            let cs = attributes
116                .get("colspan")
117                .and_then(|v| v.trim().parse::<usize>().ok())
118                .unwrap_or(1)
119                .clamp(1, 256);
120            let rs = attributes
121                .get("rowspan")
122                .and_then(|v| v.trim().parse::<usize>().ok())
123                .unwrap_or(1)
124                .clamp(1, 1024);
125            return (cs, rs);
126        }
127    }
128    (1, 1)
129}
130
131/// LayoutBox が表す要素のタグ名 (text ノードや AnonymousBlock は None)。
132pub(crate) fn box_tag_name<'a>(b: &LayoutBox<'a>) -> Option<&'a str> {
133    let styled = match &b.box_type {
134        BoxType::BlockNode(s)
135        | BoxType::InlineNode(s)
136        | BoxType::InlineBlockNode(s)
137        | BoxType::FlexNode(s)
138        | BoxType::GridNode(s)
139        | BoxType::TableNode(s)
140        | BoxType::TableRowGroupNode(s)
141        | BoxType::TableRowNode(s)
142        | BoxType::TableCellNode(s) => *s,
143        BoxType::AnonymousBlock => return None,
144    };
145    match &styled.node.node_type {
146        NodeType::Element { tag_name, .. } => Some(tag_name.as_str()),
147        NodeType::Text(_) => None,
148    }
149}
150
151/// LayoutBox が表す要素の CSS プロパティ値(flex の子サイズ判定用)。
152pub(crate) fn box_style_value<'a>(b: &LayoutBox<'a>, prop: &str) -> Option<String> {
153    let styled = match &b.box_type {
154        BoxType::BlockNode(s)
155        | BoxType::InlineNode(s)
156        | BoxType::InlineBlockNode(s)
157        | BoxType::FlexNode(s)
158        | BoxType::GridNode(s)
159        | BoxType::TableNode(s)
160        | BoxType::TableRowGroupNode(s)
161        | BoxType::TableRowNode(s)
162        | BoxType::TableCellNode(s) => *s,
163        BoxType::AnonymousBlock => return None,
164    };
165    styled.value(prop)
166}
167
168/// 指定プロパティが明示指定されているか(auto/空は未指定扱い)。
169pub(crate) fn box_has_explicit(b: &LayoutBox, prop: &str) -> bool {
170    box_style_value(b, prop)
171        .map(|v| {
172            let t = v.trim().to_lowercase();
173            !t.is_empty() && t != "auto"
174        })
175        .unwrap_or(false)
176}
177
178/// flex アイテムの flex-grow 係数(`flex-grow` 優先、無ければ `flex` ショートハンド先頭)。
179pub(crate) fn box_flex_grow(b: &LayoutBox) -> i32 {
180    if let Some(g) = box_style_value(b, "flex-grow") {
181        return g.trim().parse::<i32>().unwrap_or(0).max(0);
182    }
183    if let Some(f) = box_style_value(b, "flex") {
184        let t = f.trim().to_lowercase();
185        if t == "none" || t == "initial" {
186            return 0;
187        }
188        if t == "auto" {
189            return 1; // flex:auto = 1 1 auto
190        }
191        if let Some(first) = t.split_whitespace().next() {
192            return first.parse::<i32>().unwrap_or(0).max(0);
193        }
194    }
195    0
196}
197
198/// flex アイテムの flex-shrink 係数(`flex-shrink` 優先、無ければ `flex` ショートハンドの
199/// 2 番目、無指定は 1 = 既定で縮小可)。`flex:none` は 0、`flex:N`(単一数値)は 1。
200/// トークンが `flex` ショートハンドの basis に使える長さ値(px/%/em/rem/vh/vw)かどうかを
201/// 判定する。以前は `px`/`%`/`em` の3種のみ対応しており、`flex: 1 40vw` のような
202/// vw/vh/rem 単位の basis が length として認識されず、`box_flex_shrink` では誤って
203/// shrink の数値パースに回されて(失敗し既定値1にフォールバック)、`box_flex_basis` では
204/// 該当トークンが見つからず basis が丸ごと失われる、という実害のあるバグがあった
205/// (`transition`/`animation` の時間値誤分類と同種の「対応単位の一部だけを見る」バグ)。
206/// 末尾を剥がした残りが実際に数値としてパースできることまで確認する。
207pub(crate) fn is_flex_length_token(tok: &str) -> bool {
208    for suffix in ["px", "%", "rem", "em", "vh", "vw"] {
209        if let Some(n) = tok.strip_suffix(suffix) {
210            if n.trim().parse::<f32>().is_ok() {
211                return true;
212            }
213        }
214    }
215    false
216}
217
218pub(crate) fn box_flex_shrink(b: &LayoutBox) -> i32 {
219    if let Some(s) = box_style_value(b, "flex-shrink") {
220        return s.trim().parse::<i32>().unwrap_or(1).max(0);
221    }
222    if let Some(f) = box_style_value(b, "flex") {
223        let t = f.trim().to_lowercase();
224        if t == "none" {
225            return 0; // flex:none = 0 0 auto
226        }
227        if t == "initial" {
228            return 1; // flex:initial = 0 1 auto
229        }
230        if t == "auto" {
231            return 1; // flex:auto = 1 1 auto
232        }
233        // `flex: <grow> <shrink> <basis>` の 2 番目があれば採用。
234        let parts: Vec<&str> = t.split_whitespace().collect();
235        if let Some(second) = parts.get(1) {
236            // 2 番目が単位付き(basis)なら shrink 省略 → 既定 1。
237            if !is_flex_length_token(second) && *second != "auto" {
238                return second.parse::<i32>().unwrap_or(1).max(0);
239            }
240        }
241        // `flex: N`(単一数値)は 1 1 0 相当 → shrink=1。
242        return 1;
243    }
244    1
245}
246
247/// flex/grid アイテムの `order`(描画順の並び替え)。未指定/不正は 0。負値可。
248pub(crate) fn box_order(b: &LayoutBox) -> i32 {
249    box_style_value(b, "order")
250        .and_then(|v| v.trim().parse::<i32>().ok())
251        .unwrap_or(0)
252}
253
254/// flex アイテムの flex-basis(主軸初期サイズ)。`flex-basis` 優先、無ければ `flex`
255/// ショートハンドの長さトークン(px/%)。`auto`/`content`/未指定は None(width/height に従う)。
256/// `main_size` は % 解決の基準(コンテナ主軸長)。
257pub(crate) fn box_flex_basis(b: &LayoutBox, main_size: i32) -> Option<i32> {
258    // 明示 flex-basis を最優先。
259    if let Some(v) = box_style_value(b, "flex-basis") {
260        let t = v.trim().to_lowercase();
261        if t.is_empty() || t == "auto" || t == "content" {
262            return None;
263        }
264        return resolve_basis_len(&t, main_size);
265    }
266    // flex ショートハンドの第3トークン(長さ)から basis を拾う。
267    if let Some(f) = box_style_value(b, "flex") {
268        let t = f.trim().to_lowercase();
269        if t == "none" || t == "auto" || t == "initial" {
270            return None; // basis=auto 相当
271        }
272        // `flex: <grow> <shrink>? <basis>?` の末尾トークンが長さなら basis。
273        for tok in t.split_whitespace() {
274            if is_flex_length_token(tok) {
275                return resolve_basis_len(tok, main_size);
276            }
277        }
278        // `flex: N`(単一数値)は basis=0。
279        if t.split_whitespace().count() == 1 && t.parse::<i32>().is_ok() {
280            return Some(0);
281        }
282    }
283    None
284}
285
286/// flex-basis のトークン("40px" / "25%" / "2em")を px へ解決する。
287pub(crate) fn resolve_basis_len(tok: &str, main_size: i32) -> Option<i32> {
288    if let Some(p) = tok.strip_suffix('%') {
289        return p
290            .trim()
291            .parse::<f32>()
292            .ok()
293            .map(|pct| round_f32_to_i32(main_size as f32 * pct / 100.0).max(0));
294    }
295    parse_px_like(tok).map(|v| v.max(0))
296}
297
298/// flex アイテムの align-self(個別交差軸整列)。未指定/auto なら None(コンテナの align-items に従う)。
299pub(crate) fn box_align_self(b: &LayoutBox) -> Option<String> {
300    box_style_value(b, "align-self").and_then(|v| {
301        let t = v.trim().to_lowercase();
302        if t.is_empty() || t == "auto" {
303            None
304        } else {
305            Some(t)
306        }
307    })
308}
309
310/// grid アイテムの整列値を解決する。`self_prop`(justify-self/align-self)が
311/// auto/未指定なら、コンテナの `items_default`(justify-items/align-items の値)へ委譲。
312/// 返り値を正規化: "start"/"end"/"center"/"stretch"。`flex-start`/`flex-end`/`self-start`/
313/// `self-end` も start/end に正規化する。既定は stretch。
314pub(crate) fn grid_item_align(b: &LayoutBox, self_prop: &str, items_default: &str) -> &'static str {
315    let raw = match box_style_value(b, self_prop) {
316        Some(v) => {
317            let t = v.trim().to_lowercase();
318            if t.is_empty() || t == "auto" {
319                String::from(items_default)
320            } else {
321                t
322            }
323        }
324        None => String::from(items_default),
325    };
326    match raw.as_str() {
327        "center" => "center",
328        "end" | "flex-end" | "self-end" | "right" => "end",
329        "start" | "flex-start" | "self-start" | "left" => "start",
330        _ => "stretch",
331    }
332}
333