← 解説「位置と大きさ — block は縦に積み、inline は横に並べ、右端で折り返す」に戻る
サンプルコードで身につける: 位置と大きさ — block は縦に積み、inline は横に並べ、右端で折り返す
解説で学んだ概念を、3つの具体例で確認します。素朴な例から実務寄りの例へと進みます。
1block を縦に積む(幅は親いっぱい・高さは中身)
layout_block の骨組みだけを写したものです。幅は container_width をそのまま使い(width: auto)、子を cursor_y の位置に順に置いて、高さは子が占めた分、子が無ければ font-size × 1.5 にします(height: auto)。実行すると h1 が h=48、p が y=48 h=24、div が y=72 h=48 と縦に積まれ、body の高さが 120 になります。
Rust
// block の箱は縦に積む。幅は親いっぱい(width: auto)、高さは中身で決まる(height: auto)。
// 中身が文字 1 行だけなら、高さ = font-size × 1.5。
const VIEWPORT_WIDTH: f32 = 640.0;
const LINE_HEIGHT_RATIO: f32 = 1.5;
struct LayoutBox {
tag: &'static str,
font_size: f32,
x: f32,
y: f32,
width: f32,
height: f32,
children: Vec<LayoutBox>,
}
fn layout_block(b: &mut LayoutBox, x: f32, y: f32, container_width: f32) {
b.x = x;
b.y = y;
b.width = container_width; // auto: 親いっぱい
let mut cursor_y = y; // 「次の block をどこに置くか」
for child in b.children.iter_mut() {
layout_block(child, x, cursor_y, b.width);
cursor_y = child.y + child.height; // 前の block の真下
}
// auto: 子があれば子が占めた分、無ければ(文字 1 行として)font-size × 1.5
b.height = if b.children.is_empty() { b.font_size * LINE_HEIGHT_RATIO } else { cursor_y - y };
}
fn dump(b: &LayoutBox, depth: usize) {
println!("{}<{}> x={:.0} y={:.0} w={:.0} h={:.0}", " ".repeat(depth), b.tag, b.x, b.y, b.width, b.height);
for c in &b.children {
dump(c, depth + 1);
}
}
fn block(tag: &'static str, font_size: f32, children: Vec<LayoutBox>) -> LayoutBox {
LayoutBox { tag, font_size, x: 0.0, y: 0.0, width: 0.0, height: 0.0, children }
}
fn main() {
// <body><h1>..</h1><p>..</p><div><p>..</p><p>..</p></div></body> h1 は 32px、他は 16px
let mut body = block("body", 16.0, vec![
block("h1", 32.0, vec![]),
block("p", 16.0, vec![]),
block("div", 16.0, vec![block("p", 16.0, vec![]), block("p", 16.0, vec![])]),
]);
layout_block(&mut body, 0.0, 0.0, VIEWPORT_WIDTH);
dump(&body, 0);
}
2カーソルで行を管理し、右端で折り返す
参照実装 layout_flow の Text の枝をそのまま切り出した layout_text です。Cursor の x, y, line_height だけで「今の行のどこまで埋まったか」を持ち、この行に入る文字数 avail が尽きたら newline() で次の行へ移ります。幅 160px(16px の文字で 20 文字)に 3 つの文字列を流すと、"0" が x=64、3 つ目が x=72 から始まって y=24 の行に、 の高さは 48px(2 行)になります。
Rust
// inline と文字は横に並べ、右端に届いたら折り返す。行の管理はカーソル(x, y, line_height)1 つで足りる。
// 1 文字の幅 = font-size × 0.5、行の高さ = font-size × 1.5(等幅フォントの近似)。
const CHAR_WIDTH_RATIO: f32 = 0.5;
const LINE_HEIGHT_RATIO: f32 = 1.5;
/// 折り返し後の 1 行分の文字列と位置
struct TextLine { text: String, x: f32, y: f32 }
/// 行の中の「今どこまで埋まったか」
struct Cursor { x: f32, y: f32, left: f32, right: f32, line_height: f32 }
impl Cursor {
fn newline(&mut self) {
self.y += self.line_height;
self.x = self.left;
self.line_height = 0.0;
}
}
/// 文字列 1 つを行に流し込む(参照実装 layout_flow の Text の枝)。入り切らなければ次の行へ
fn layout_text(text: &str, font_size: f32, cur: &mut Cursor) -> Vec<TextLine> {
let char_w = font_size * CHAR_WIDTH_RATIO;
let line_h = font_size * LINE_HEIGHT_RATIO;
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();
}
}
lines
}
fn main() {
// 幅 160px の block の中(16px の文字なら 1 行 20 文字)
let mut cur = Cursor { x: 0.0, y: 0.0, left: 0.0, right: 160.0, line_height: 0.0 };
let texts = ["Clicks: ", "0", " and a long sentence that wraps"]; // 文字 / <span> の文字 / 文字
for t in texts.iter() {
for line in layout_text(t, 16.0, &mut cur) {
println!("x={:>3.0} y={:>2.0} {:?}", line.x, line.y, line.text);
}
}
if cur.line_height > 0.0 {
cur.newline(); // 最後の行を確定する
}
println!("block の高さ = {:.0}px({} 行 × 24px)", cur.y, (cur.y / 24.0) as i32);
}
3実務に写す: 端末に出す表の列幅
固定幅の列はその幅、auto の列は残りを分け合う resolve_widths と、セルの文字を列幅で折り返す wrap です。行の高さが一番高いセルで決まるのは、block の高さが中身で決まるのと同じ考え方です。実行すると列幅が [6, 8, 22] と決まり、長い Note が 3 行に折り返された表が出ます。
Rust
// 実務に写す: 端末に出す表の列幅を決める。固定幅の列はその幅、auto の列は残りを分け合う(width: auto)。
// セルの文字は列幅で折り返し、行の高さは一番高いセルで決まる(height: auto)。考え方はブラウザの箱と同じ。
struct Column { title: &'static str, width: Option<usize> } // None = auto
fn resolve_widths(cols: &[Column], total: usize) -> Vec<usize> {
let separators = cols.len() + 1; // 両端と列の間の "|"
let fixed: usize = cols.iter().filter_map(|c| c.width).sum();
let autos = cols.iter().filter(|c| c.width.is_none()).count().max(1);
let share = (total - separators - fixed) / autos;
cols.iter().map(|c| c.width.unwrap_or(share)).collect()
}
/// 文字を幅で折り返す(単語境界は見ない。参照実装と同じ)
fn wrap(text: &str, width: usize) -> Vec<String> {
let chars: Vec<char> = text.chars().collect();
chars.chunks(width).map(|c| c.iter().collect()).collect()
}
fn print_row(cells: &[&str], widths: &[usize]) {
let wrapped: Vec<Vec<String>> = cells.iter().zip(widths).map(|(c, w)| wrap(c, *w)).collect();
let height = wrapped.iter().map(|w| w.len()).max().unwrap_or(1); // 行の高さ = 一番高いセル
for i in 0..height {
let mut line = String::from("|");
for (col, w) in wrapped.iter().zip(widths) {
let text = col.get(i).map(|s| s.as_str()).unwrap_or("");
line.push_str(&format!("{:<w$}|", text, w = *w));
}
println!("{}", line);
}
}
fn main() {
let cols = [
Column { title: "ID", width: Some(6) },
Column { title: "Status", width: Some(8) },
Column { title: "Note", width: None }, // 残りを全部もらう
];
let total = 40; // 端末の幅(桁)
let widths = resolve_widths(&cols, total);
println!("列幅: {:?}(合計 {} 桁のうち区切りが {} 桁)", widths, total, cols.len() + 1);
let titles: Vec<&str> = cols.iter().map(|c| c.title).collect();
print_row(&titles, &widths);
println!("{}", "-".repeat(total));
print_row(&["T-101", "open", "Login fails on Safari 17 when cookies are blocked"], &widths);
print_row(&["T-102", "closed", "Typo in footer"], &widths);
}