← 解説「表示リストと描画 — 「何をどこに描くか」の命令列にしてから描く」に戻る
サンプルコードで身につける: 表示リストと描画 — 「何をどこに描くか」の命令列にしてから描く
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1表示リスト: 背景の Rect を先に、文字の Text を後に
を の列にする と paint_box です。自分の背景(あれば Rect)を積んでから子を辿るので、親の背景が先、文字が後になり、後に描いたものが上に来ます。実行すると p の黄色い Rect と赤い文字の Text の 2 命令が出て、背景が transparent の body は何も出さないことが分かります。
Rust
// 表示リスト(display list): 箱の木を「何をどこに描くか」の命令列に変える。命令は Rect(背景の四角)と Text(1 行の文字)の 2 種。
// 親の背景を先に、子と文字を後に積む。後に描いたものが上に来るので、文字が背景に隠れない。
#[derive(Clone, Copy)]
struct Color { r: u8, g: u8, b: u8 }
impl Color {
fn hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
}
enum DisplayItem {
Rect { x: f32, y: f32, width: f32, height: f32, color: Color },
Text { x: f32, y: f32, text: String, color: Color },
}
struct TextLine { text: String, x: f32, y: f32 }
enum BoxKind { Block, Text } // 参照実装は Text(String) で折りたたみ後の文字列も持つ。ここでは lines だけ使う
struct LayoutBox {
kind: BoxKind,
x: f32, y: f32, width: f32, height: f32,
background_color: Option<Color>, // None = transparent
color: Color,
lines: Vec<TextLine>, // Text の箱だけが持つ
children: Vec<LayoutBox>,
}
/// 箱の木 → 表示リスト。自分の背景 → 子(文字)の順
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 => {
if let Some(bg) = b.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.color });
}
}
}
for c in &b.children {
paint_box(c, items); // 子は自分より後 = 上に描かれる
}
}
fn main() {
let (yellow, red, black) = (Color { r: 255, g: 255, b: 0 }, Color { r: 255, g: 0, b: 0 }, Color { r: 0, g: 0, b: 0 });
// <body><p class="note">Clicks: 3</p></body> .note { background-color: yellow; color: red }
let text = LayoutBox {
kind: BoxKind::Text, x: 0.0, y: 0.0, width: 72.0, height: 24.0,
background_color: None, color: red,
lines: vec![TextLine { text: "Clicks: 3".to_string(), x: 0.0, y: 0.0 }], children: vec![],
};
let p = LayoutBox {
kind: BoxKind::Block, x: 0.0, y: 0.0, width: 640.0, height: 24.0,
background_color: Some(yellow), color: red, lines: vec![], children: vec![text],
};
let body = LayoutBox {
kind: BoxKind::Block, x: 0.0, y: 0.0, width: 640.0, height: 24.0,
background_color: None, color: black, lines: vec![], children: vec![p],
};
for (i, item) in paint(&body).iter().enumerate() {
match item {
DisplayItem::Rect { x, y, width, height, color } => {
println!("{}: Rect x={:.0} y={:.0} w={:.0} h={:.0} color={}", i, x, y, width, height, color.hex())
}
DisplayItem::Text { x, y, text, color } => println!("{}: Text x={:.0} y={:.0} {:?} color={}", i, x, y, text, color.hex()),
}
}
println!("body は背景が transparent なので命令を出さない。p の Rect が先、文字が後");
}
2文字のキャンバスに写す(1 セル = 8×24px)
表示リストを 1 セル 8×24px の格子に描く です。Rect は '░' で塗り、Text は同じセルを文字で上書きするので、命令の順番がそのまま重なり順になります。幅 160px(20 セル)で実行すると、32px の Hello が 2 行分の高さを取り、黄色い Rect の上に Clicks: 3 が重なった枠が出ます。白の Rect は地の色なので何も描きません。
Rust
// 表示リストを「文字のキャンバス」に写す。1 セル = 8×24px(16px の文字 1 つが 1 セル)。
// 命令を先頭から順に実行するので、Rect が塗った '░' の上に、後の Text の文字が重なる。
#[derive(Clone, Copy, PartialEq)]
struct Color { r: u8, g: u8, b: u8 }
impl Color {
const WHITE: Color = Color { r: 255, g: 255, b: 255 };
}
enum DisplayItem {
Rect { x: f32, y: f32, width: f32, height: f32, color: Color },
Text { x: f32, y: f32, text: String, font_size: f32 },
}
const CELL_WIDTH: f32 = 8.0;
const CELL_HEIGHT: f32 = 24.0;
fn cell(px: f32, size: f32) -> usize {
(px / size).floor().max(0.0) as usize
}
/// 表示リストを文字のキャンバスに描く。幅は px で与える
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, r1) = (cell(x + width, CELL_WIDTH).min(cols), 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, .. } => {
let (mut c, r) = (cell(*x, CELL_WIDTH), cell(*y, CELL_HEIGHT));
for ch in text.chars() {
if c >= cols || r >= rows {
break;
}
grid[r][c] = ch; // 先に塗った '░' を上書きする
c += 1;
}
}
}
}
let mut out = format!("┌{}┐\n", "─".repeat(cols));
for row in grid {
out.push('│');
out.extend(row);
out.push_str("│\n");
}
out + &format!("└{}┘", "─".repeat(cols))
}
fn main() {
let yellow = Color { r: 255, g: 255, b: 0 };
// <h1>Hello</h1><p class="note">Clicks: 3</p> を幅 160px(20 セル)で。h1 は 32px なので 2 セル分の高さ
let items = vec![
DisplayItem::Text { x: 0.0, y: 0.0, text: "Hello".to_string(), font_size: 32.0 },
DisplayItem::Rect { x: 0.0, y: 48.0, width: 160.0, height: 24.0, color: yellow },
DisplayItem::Text { x: 0.0, y: 48.0, text: "Clicks: 3".to_string(), font_size: 16.0 },
DisplayItem::Rect { x: 0.0, y: 72.0, width: 160.0, height: 24.0, color: Color::WHITE },
];
println!("{}", render_ascii(&items, 160.0));
}
3実務に写す: 端末ダッシュボードの描画命令列
使用率の一覧を Rect(棒)と Text(名前と数値)の表示リストにしてから、render_ascii で描きます。「何を描くか」が命令列として手に取れるので、端末が無くてもテストで命令の数や棒の幅を検査できます。実行すると命令 12 個(うち Rect 4 個)と、4 本の棒グラフが出ます。
Rust
// 実務に写す: 端末のダッシュボード。「何を描くか」(表示リスト)と「どう描くか」(キャンバス)を分けると、
// 描く内容のテストが端末なしでできる。使用率の棒は Rect、ラベルは Text。後の命令が上に来るので、棒の上に数字を重ねられる。
enum DisplayItem {
Rect { x: f32, y: f32, width: f32, height: f32 },
Text { x: f32, y: f32, text: String },
}
const CELL_WIDTH: f32 = 8.0;
const CELL_HEIGHT: f32 = 24.0;
const LABEL_WIDTH: f32 = 80.0; // 名前の列 = 10 セル
const BAR_WIDTH: f32 = 160.0; // 100% のときの棒の長さ = 20 セル
/// 使用率の一覧 → 表示リスト。描く順 = 重なる順
fn build_display_list(rows: &[(&str, f32)]) -> Vec<DisplayItem> {
let mut items = Vec::new();
for (i, (name, percent)) in rows.iter().enumerate() {
let y = i as f32 * CELL_HEIGHT;
items.push(DisplayItem::Text { x: 0.0, y, text: name.to_string() });
items.push(DisplayItem::Rect { x: LABEL_WIDTH, y, width: BAR_WIDTH * percent / 100.0, height: CELL_HEIGHT });
items.push(DisplayItem::Text { x: LABEL_WIDTH + BAR_WIDTH + CELL_WIDTH, y, text: format!("{:>3.0}%", percent) });
}
items
}
fn cell(px: f32, size: f32) -> usize {
(px / size).floor().max(0.0) as usize
}
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, .. } => y + CELL_HEIGHT,
}).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 } => {
let (c0, r0) = (cell(*x, CELL_WIDTH), cell(*y, CELL_HEIGHT));
let (c1, r1) = (cell(x + width, CELL_WIDTH).min(cols), 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 } => {
let (mut c, r) = (cell(*x, CELL_WIDTH), cell(*y, CELL_HEIGHT));
for ch in text.chars() {
if c >= cols || r >= rows {
break;
}
grid[r][c] = ch;
c += 1;
}
}
}
}
let mut out = String::new();
for row in grid {
out.push('|');
out.extend(row);
out.push_str("|\n");
}
out
}
fn main() {
let usage = [("web-1", 72.0), ("web-2", 35.0), ("db", 100.0), ("batch", 5.0)];
let items = build_display_list(&usage);
// 端末が無くても、命令列の中身は検査できる(テストならここを assert にする)
let rects = items.iter().filter(|i| matches!(i, DisplayItem::Rect { .. })).count();
println!("命令 {} 個(うち Rect {} 個)。db の棒の幅 = {:.0}px", items.len(), rects, BAR_WIDTH);
print!("{}", render_ascii(&items, LABEL_WIDTH + BAR_WIDTH + CELL_WIDTH * 5.0));
}