21. 箱の木 — block・inline・text の 3 種類と、箱にならないもの
レイアウトの入力は ではなく「(box tree)」です。はの display に従ってになり、 の要素は子孫ごと箱を作りません。CSS Display Level 3 の言葉では、 は block-level の箱、 は inline-level の箱、none は「その要素と子孫は箱を生成しない」です。文字は親の計算値を引き継いだテキストの箱になります。
参照実装の build_layout_tree は DOM を再帰的に辿ります。Element なら compute_style で計算値を出し、None なら None を返して枝ごと落とす、Block なら BoxKind::Block、Inline なら BoxKind::Inline の LayoutBox を作って子を再帰。Text なら、連続する空白を 1 つにまとめ(CSS Text の white-space: normal の折りたたみ)、空白だけなら箱にしない。だから HTML の改行とインデントは画面に出ません。 は箱を作らず、body(と html)だけを根にします。
head / style / script / title は 既定で display: none なので、箱の木には現れません。これが「<style> の中身や <script> の中身が画面に出ない」理由です。もし作者が style { display: block } と書けば、本物のブラウザでも CSS のソースが画面に出ます。
下のコードは build_layout_tree と、箱の木を字下げして表示する dump_layout です。前のステップ(step09)を実行すると、block が縦に並ぶ例、inline と文字が横に並ぶ例、display: none で箱が消える例、空白だけの文字が箱にならない例が、座標つきで出ます。座標の決め方は次のレッスンです。
本物のブラウザでは: DOM と計算値から作る『描くための木』を、 は の木(core/layout/layout_object.h)、Firefox は の frame tree と呼びます。Firefox の文書は「display:none の要素には frame が無い」「行で折れた内容は複数の frame になり、最初を primary frame、続きを continuation frame と呼ぶ」と説明していて、frame を作るのは nsCSSFrameConstructor です。この回の build_layout_tree() が display: none を落とすのは同じ理由です。違いは、本物は 1 要素が複数の箱( / continuation)になれることです。
開発のどこに活かすか: 「DOM に要素はあるが箱は無い(display: none)」と「箱はあるが見えない(visibility: hidden や opacity: 0)」の違いは、ここで分かれます。前者は幅も高さも 0 で場所を取らず、後者は場所を取ります。アクセシビリティツリーやテストツール(「表示されているか」の判定)が何を見ているかも、箱の木の有無で説明がつきます。
点線の付いた語はタップすると説明が出ます。用語集を開く →
① DOM と箱の木は 1 対 1 ではない
- ・head とその中身も UA 既定の display: none で消える
- ・<p> と <p> の間の改行だけのテキストは「空白のみ」なので箱にならない
レイアウトと描画はこの木だけを見ます。DOM に戻ることはありません。
サンプルコード(フレームワーク環境が必要なため表示のみ)
// src/renderer/layout.rs の箱の木を作る部分
pub fn build_layout_tree(node: &NodeRef, sheet: &StyleSheet, parent_style: Option<&ComputedStyle>) -> Option<LayoutBox> {
match &node.kind {
NodeKind::Document => {
// document 自身は箱を作らない。最初に箱になった子(html)を返す
node.children()
.iter()
.find_map(|c| build_layout_tree(c, sheet, parent_style))
}
NodeKind::Element(el) => {
let style = compute_style(el, sheet, parent_style);
let kind = match style.display {
Display::None => return None,
Display::Block => BoxKind::Block,
Display::Inline => BoxKind::Inline,
};
let mut b = LayoutBox::new(kind, &el.tag, style);
for child in node.children() {
if let Some(cb) = build_layout_tree(&child, sheet, Some(&b.style)) {
b.children.push(cb);
}
}
Some(b)
}
NodeKind::Text(text) => {
// 連続する空白は 1 つに(CSS Text の white-space: normal の折りたたみ)。空白だけなら箱にしない
let collapsed = collapse_whitespace(text);
if collapsed.trim().is_empty() {
return None;
}
let style = parent_style.cloned().unwrap_or_default();
Some(LayoutBox::new(BoxKind::Text(collapsed), "#text", style))
}
}
}
手元で動かす(配布物)
- ステップ 9 の 1 ファイル版(step09_layout.rs)実行すると箱の木が座標つきで出る。テスト 45 本
図やコードに出てくる用語(タップで説明)
学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。
公式ドキュメントで詳しく ↗