ペインを分割する仕組み — 二分木の話

ペインを縦に割って、右側をまた横に割って、その中をまた…。この入れ子の分割が自然に書けるのは、中身が二分木だから。

ccmux でも tmux でも VS Code のターミナルでも、ペインを分割していくとこんな形になります:

┌─────────┬──────────────┐
│ │ │
│ │ pane B │
│ pane A │ │
│ ├──────────────┤
│ │ │
│ │ pane C │
└─────────┴──────────────┘

これを頭の中で「どう管理してるんだろう」と考えると、実は二分木が一番シンプルな答えになります。

木として見る

さっきの図は、こういう木です:

縦分割 (50:50)
/ \
pane A 横分割 (60:40)
/ \
pane B pane C
  • 葉はペインそのもの
  • 分岐ノードは分割(どっち向き・どんな比率)

ccmux ではこう書いてます:

src/layout.rs
pub enum LayoutNode {
Leaf {
pane_id: usize,
},
Split {
direction: SplitDirection,
ratio: f32, // 0.0 〜 1.0
first: Box<LayoutNode>,
second: Box<LayoutNode>,
},
}

3 ペインでも 16 ペインでも、同じ構造の木で表せます。分岐を増やせば、任意の入れ子が作れるのがいいところ。

分割 = 葉を分岐に置き換える

Ctrl+D で縦分割する時にやってることは一行で説明できます:

今フォーカスしているを、分岐ノードに置き換える。分岐の片方は元の葉、もう片方は新しいペイン。

fn split_focused(leaf: LayoutNode, new_pane: usize) -> LayoutNode {
LayoutNode::Split {
direction: SplitDirection::Horizontal,
ratio: 0.5,
first: Box::new(leaf),
second: Box::new(LayoutNode::Leaf { pane_id: new_pane }),
}
}

木が深くなっていくだけで、他は何も変わらない。

描画 = 領域を再帰で分ける

画面全体の長方形を、葉に行き着くまで再帰的に割っていきます:

src/layout.rs
fn compute_layout(node: &LayoutNode, area: Rect) -> Vec<(usize, Rect)> {
match node {
LayoutNode::Leaf { pane_id } => {
vec![(*pane_id, area)]
}
LayoutNode::Split { direction, ratio, first, second } => {
let (a, b) = split_rect(area, *direction, *ratio);
let mut out = compute_layout(first, a);
out.extend(compute_layout(second, b));
out
}
}
}

毎フレーム呼ばれるけど、ノードはせいぜい数十個なので軽いです。ratio を変えれば即リサイズ。

閉じる = 残った兄弟で置き換える

Ctrl+W でペインを閉じた時、空っぽになった分岐ノードを残してはいけません。代わりに「残った兄弟ノード」を親の位置にそのまま昇格させます。

分岐
/ \
pane A pane B ← ここを閉じる

pane A ← 親の位置に pane A が上がる

これを再帰で実装すれば、どれだけ深い入れ子でも自動的に綺麗に片付きます。

マウスで境界をドラッグしてリサイズ

描画のついでに「境界の位置」も一緒に記録しておけば、マウスの座標と照らし合わせてドラッグ対象がわかります。

pub struct Border {
pub node_path: Vec<usize>, // ルートから対象 Split への経路
pub line_pos: u16, // 境界の座標
}

ドラッグ中は path を頼りに対象の Split を取り出して、ratio を更新するだけ:

*ratio = (mouse_x - area.x) as f32 / area.width as f32;
*ratio = ratio.clamp(0.15, 0.85); // 端まで寄せすぎ防止

clamp を入れないと、ユーザーが勢いよくドラッグしたペインが幅ゼロになって見えなくなる。地味だけど大事な一行。

なぜ N 分木ではなく二分木?

「3 ペインを均等に並べたい時に (A|B)|C みたいな非対称になるのでは?」

そうなります。でも ratio を 1/3 にすれば見た目は 3 等分なので、ユーザーには気づかれません。

一方で、挿入・削除・リサイズのロジックが一気にシンプルになる。「分割する」「閉じる」の 2 つの操作しか使わないなら、二分木が明らかに勝ちです。


ペイン分割 UI は、見た目ほど難しい問題ではありません。二分木と再帰が書ければ、誰でも作れる。マルチプレクサを作ってみたい人の最初のハードルは、意外と低いです。