22. 位置と大きさ — block は縦に積み、inline は横に並べ、右端で折り返す
(normal flow)の規則は 2 つだけです。 のは縦に積まれ、幅は親いっぱい、高さは中身で決まる。 の箱と文字は横に並び、右端に達したら次の行へ折り返す。CSS 2 §9 の visual formatting model のうち、float や position を使わないときの振る舞いがこれです。margin / padding / border は省いているので、箱の位置は直前の箱の終わりから始まります。
参照実装は、幅を上から下へ、高さを下から上へ決めます。layout_block は自分の x, y を親から受け取り、幅を style.width か親の幅(auto)にしてから子を置き、全部置き終わった y から自分の高さ(auto)を求めます。だから親の高さは子の配置が終わるまで分かりません。文字の大きさは等幅フォントの近似で、1 文字 = font-size × 0.5 、1 行 = font-size × 1.5 px です。の幅は 640px(80 文字)にしてあります。
行の管理は Cursor(x, y, 行の左端 left、右端 right、今の行の高さ line_height)です。文字の箱は、右端までに入る文字数だけを 1 行に置き、残りがあれば newline(y を line_height 分進めて x を left に戻す)して続きを置きます。単語の境界は見ず文字単位でます(本物は単語境界と禁則を見ます)。h1 の 32px の文字が混じる行は line_height がその分高くなり、行の高さが一番大きい文字で決まる、という本物と同じ性質が出ます。
block の子が来たら、まず今の行を確定(newline)してから、その block を cur.left, cur.y に置き、終わったら y を block の高さだけ進めます。inline の子は Cursor をそのまま子に渡して中の文字を続けて置かせます。前のステップの実行結果で、<p>ab<a>cd</a>ef</p> の <a> が x=16(2 文字 × 8px)から始まり、100 文字の段落が 80 文字で折り返して高さが 48(2 行)になるのを確かめてください。
本物のブラウザでは: の は、入力を BlockNode(と)と (親から来る制約)に限り、「現在のレイアウトはこの集合の外の情報に触ってはならない」と決めています。出力は書き換え不可の の木で、同じ入力なら前回の結果を再利用します(CachedLayoutResult)。アルゴリズムは display ごとに分かれ、行の折り返しは LineBreaker です。この回の Cursor を渡す layout_block() は ConstraintSpace の最小版、 を直接書き換える点は LayoutNG 以前の形です。Firefox は と呼び、ReflowInput(親の制約)と ReflowOutput(結果)を使い、NS_FRAME_IS_DIRTY のビットで必要な部分だけやり直します。文字幅は本物は で実測し(Blink の harfbuzz_shaper.cc、Firefox の gfx/harfbuzz)、この実装の文字数 × 比率は近似です。
開発のどこに活かすか: 「幅は親から、高さは子から」は、CSS の height: 100% が思いどおりにならない理由(親の高さがまだ決まっていない)や、レイアウトの計算コスト(子を全部置くまで親が確定しない=木全体を 1 回なめる)の理解に直結します。 を 1 要素ずつ触って毎回レイアウトを起こすと遅い、まとめて書き換えて 1 回で済ませる、という実務の指針は、この再計算の重さから来ています。
点線の付いた語はタップすると説明が出ます。用語集を開く →
① Cursor が行を進める
- ・100 文字の文字列なら 80 文字で newline が入り、2 行目は y=24。段落の高さは 48
- ・次の block はこの高さの分だけ下から始まる
Cursor を子に渡して返してもらう、という 1 本の流れで、inline の入れ子も折り返しも扱えます。
サンプルコード(フレームワーク環境が必要なため表示のみ)
// src/renderer/layout.rs の位置と大きさを決める部分
fn layout_block(b: &mut LayoutBox, x: f32, y: f32, container_width: f32) {
b.x = x;
b.y = y;
// 幅: 指定があればそれ、無ければ親いっぱい(auto)
b.width = b.style.width.unwrap_or(container_width);
let width = b.width;
let mut cur = Cursor {
x,
y,
left: x,
right: x + width,
line_height: 0.0,
};
layout_flow(&mut b.children, &mut cur, width);
if cur.line_height > 0.0 {
cur.newline(); // 最後の行を確定する
}
// 高さ: 指定があればそれ、無ければ中身の分(auto)
b.height = b.style.height.unwrap_or(cur.y - y);
}
fn layout_flow(children: &mut [LayoutBox], cur: &mut Cursor, container_width: f32) {
for child in children.iter_mut() {
match child.kind.clone() {
BoxKind::Block => {
if cur.line_height > 0.0 {
cur.newline();
}
layout_block(child, cur.left, cur.y, container_width);
cur.y = child.y + child.height;
cur.x = cur.left;
cur.line_height = 0.0;
}
BoxKind::Inline => {
child.x = cur.x;
child.y = cur.y;
let start_y = cur.y;
layout_flow(&mut child.children, cur, container_width);
// inline の箱の大きさは「子が占めた範囲」の近似
child.width = if cur.y == start_y { cur.x - child.x } else { container_width };
child.height = (cur.y + cur.line_height) - start_y;
}
BoxKind::Text(text) => {
let font = child.style.font_size;
let char_w = font * CHAR_WIDTH_RATIO;
let line_h = font * LINE_HEIGHT_RATIO;
child.x = cur.x;
child.y = cur.y;
let chars: Vec<char> = text.chars().collect();
let mut rest: &[char] = &chars;
let mut lines = Vec::new();
while !rest.is_empty() {
let mut avail = ((cur.right - cur.x) / char_w).floor() as usize;
if avail == 0 {
if cur.x > cur.left {
cur.newline(); // 行末に来たので折り返す
continue;
}
avail = 1; // 行頭でも 1 文字入らないなら、はみ出してでも 1 文字置く
}
let take = avail.min(rest.len());
let (head, tail) = rest.split_at(take);
lines.push(TextLine {
text: head.iter().collect(),
x: cur.x,
y: cur.y,
});
cur.x += take as f32 * char_w;
cur.line_height = cur.line_height.max(line_h);
rest = tail;
if !rest.is_empty() {
cur.newline();
}
}
child.lines = lines;
child.width = (text.chars().count() as f32 * char_w).min(container_width);
child.height = (cur.y + cur.line_height) - child.y;
}
}
}
}
手元で動かす(配布物)
- ステップ 9 の 1 ファイル版(step09_layout.rs)blocks_stack_vertically / inline_flows_horizontally / long_text_wraps のテストがこの回の主題
学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。
公式ドキュメントで詳しく ↗