Dev Study
← Rust で小さなブラウザを作る コース

23. 表示リストと描画 — 「何をどこに描くか」の命令列にしてから描く

本物のブラウザは、レイアウトの結果を直接画面に描きません。一度「何をどこに描くか」の命令列(、display list)にしてから、それを GPU に渡します。描く対象()と描く手段(画面・プリンタ・画像ファイル)を切り離すためです。この実装も同じ順で、 が LayoutBox の木から DisplayItem の列を作り、 がそれを文字のキャンバスに描きます。

DisplayItem は 2 種類です。Rect(背景の四角: 位置・大きさ・色)と Text(1 行分の文字列: 位置・文字列・色・font-size・下線の有無)。paint_box は、に background-color があれば先に Rect を出し、文字の箱なら後の各行を Text として出し、それから子を辿ります。親の背景を先に、子を後に出すので、後に描いたものが上に重なる(文字が背景の上に見える)ことになります。本物の描画順(stacking)はもっと複雑ですが、骨格は同じです。

render_ascii は、1 セルを 8×24px(16px の文字 1 つが 1 行に収まる大きさ)として、幅 640px なら 80 列のグリッドを作り、Rect は白以外なら網掛け、Text は文字をそのセル位置に書きます。font-size が大きい文字も 1 セル 1 文字で書くので、本物と違って大きくは見えませんが、行の高さは広がります。下線は空白のところを _ にして示します。

前のステップ(step10)を実行すると、表示リストの全命令(Rect が 1 つ、Text が行ごと)と、文字のキャンバスが出ます。表示リストの Text の x, y がレイアウトで決めた座標と一致していること、 の段落が命令に現れないことを確かめてください。

本物のブラウザでは: は BoxFragmentPainter が(display item)を PaintController に積み、同じ座標変換・クリップを共有する連続した項目を にまとめます。PaintController は「前回の描画結果を表示項目のキャッシュとして保つ」ので、変わらない部分は描き直しません。その後 PaintArtifactCompositor が の列に変換し、cc がの木 → pending tree → active tree へ commit / activate して、タイルに分けてワーカースレッドでし、の が各クライアントのフレームを 1 枚に合成します(docs/how_cc_works.md、components/viz/README.md)。Firefox は nsDisplayList を の表示リストに直列化して GPU プロセスへ送り、Rust の WebRender が Scene → Frame → GPU 命令にします。この回の DisplayItem と文字キャンバスは、表示リストとラスタライズを 1 段に潰したものです。

開発のどこに活かすか: 「描く内容」と「描く手段」を命令列で切り離す形は、Canvas や PDF 生成、帳票、グラフ描画ライブラリ、UI の(React の木と への反映、Flutter のレイヤー)に共通する設計です。レンダリングを自前で書く場面がなくても、「レイアウトは済んでいるのに描画が重い」と「レイアウトが重い」を切り分けられる(開発者ツールの Paint と Layout の計測が別物である)根拠になります。

点線の付いた語はタップすると説明が出ます。用語集を開く →

サンプルコード(フレームワーク環境が必要なため表示のみ)

// src/renderer/paint.rs(表示リストと文字のキャンバス)
//! 描画。箱の木を「表示リスト(display list)」= 描く命令の列に変え、さらに文字のキャンバスに描く。
//!
//! 本物のブラウザも、レイアウトの結果を直接画面に描くのではなく、
//! 一度「何をどこに描くか」の命令列(display list / paint record)にしてから GPU に渡す。
//! この実装はウィンドウを持たないので、命令列を 1 文字 = 8×16px のセルからなるキャンバスに描いて
//! 標準出力に出す。

use crate::renderer::layout::{BoxKind, LayoutBox};
use crate::renderer::style::{Color, TextDecoration};

#[derive(Debug, Clone, PartialEq)]
pub enum DisplayItem {
    /// 背景の四角
    Rect {
        x: f32,
        y: f32,
        width: f32,
        height: f32,
        color: Color,
    },
    /// 1 行分の文字列
    Text {
        x: f32,
        y: f32,
        text: String,
        color: Color,
        font_size: f32,
        underline: bool,
    },
}

/// 箱の木 → 表示リスト。親の背景を先に、文字を後に(後に描いたものが上に来る)
pub fn paint(root: &LayoutBox) -> Vec<DisplayItem> {
    let mut items = Vec::new();
    paint_box(root, &mut items);
    items
}

fn paint_box(b: &LayoutBox, items: &mut Vec<DisplayItem>) {
    match &b.kind {
        BoxKind::Block | BoxKind::Inline => {
            if let Some(bg) = b.style.background_color {
                items.push(DisplayItem::Rect {
                    x: b.x,
                    y: b.y,
                    width: b.width,
                    height: b.height,
                    color: bg,
                });
            }
        }
        BoxKind::Text(_) => {
            for line in &b.lines {
                items.push(DisplayItem::Text {
                    x: line.x,
                    y: line.y,
                    text: line.text.clone(),
                    color: b.style.color,
                    font_size: b.style.font_size,
                    underline: b.style.text_decoration == TextDecoration::Underline,
                });
            }
        }
    }
    for c in &b.children {
        paint_box(c, items);
    }
}

/// 1 セルの大きさ(px)。16px の文字 1 つが 1 セルに収まる想定
pub const CELL_WIDTH: f32 = 8.0;
pub const CELL_HEIGHT: f32 = 24.0; // 16px × 行の高さの比 1.5

/// 表示リストを文字のキャンバスに描く。幅は px で与える
pub fn render_ascii(items: &[DisplayItem], width_px: f32) -> String {
    let cols = (width_px / CELL_WIDTH).ceil() as usize;
    let bottom = items
        .iter()
        .map(|i| match i {
            DisplayItem::Rect { y, height, .. } => y + height,
            DisplayItem::Text { y, font_size, .. } => y + font_size * 1.5,
        })
        .fold(0.0_f32, f32::max);
    let rows = (bottom / CELL_HEIGHT).ceil().max(1.0) as usize;
    let mut grid = vec![vec![' '; cols]; rows];

    for item in items {
        match item {
            DisplayItem::Rect { x, y, width, height, color } => {
                if *color == Color::WHITE {
                    continue;
                }
                let (c0, r0) = (cell(*x, CELL_WIDTH), cell(*y, CELL_HEIGHT));
                let c1 = cell(x + width, CELL_WIDTH).min(cols);
                let r1 = cell(y + height, CELL_HEIGHT).min(rows);
                for row in grid.iter_mut().take(r1).skip(r0) {
                    for ch in row.iter_mut().take(c1).skip(c0) {
                        *ch = '░';
                    }
                }
            }
            DisplayItem::Text { x, y, text, underline, .. } => {
                let (mut c, r) = (cell(*x, CELL_WIDTH), cell(*y, CELL_HEIGHT));
                if r >= rows {
                    continue;
                }
                for ch in text.chars() {
                    if c >= cols {
                        break;
                    }
                    grid[r][c] = if *underline && ch == ' ' { '_' } else { ch };
                    c += 1;
                }
            }
        }
    }

    let mut out = String::new();
    out.push('┌');
    out.push_str(&"─".repeat(cols));
    out.push_str("┐\n");
    for row in grid {
        out.push('│');
        out.extend(row);
        out.push_str("│\n");
    }
    out.push('└');
    out.push_str(&"─".repeat(cols));
    out.push('┘');
    out
}

fn cell(px: f32, size: f32) -> usize {
    (px / size).floor().max(0.0) as usize
}

/// 表示リストを 1 行 1 命令で並べる(学習用)
pub fn dump_display_list(items: &[DisplayItem]) -> String {
    items
        .iter()
        .map(|i| match i {
            DisplayItem::Rect { x, y, width, height, color } => {
                format!("Rect  x={x:.0} y={y:.0} w={width:.0} h={height:.0} color={}", color.hex())
            }
            DisplayItem::Text { x, y, text, color, font_size, underline } => format!(
                "Text  x={x:.0} y={y:.0} {text:?} color={} size={font_size:.0}px{}",
                color.hex(),
                if *underline { " underline" } else { "" }
            ),
        })
        .collect::<Vec<_>>()
        .join("\n")
}

手元で動かす(配布物)

図やコードに出てくる用語(タップで説明)

サンプルコードで身につける(3本) →

学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。

公式ドキュメントで詳しく ↗