ペインを分割する仕組み — 二分木の話
ペインを縦に割って、右側をまた横に割って、その中をまた…。この入れ子の分割が自然に書けるのは、中身が二分木だから。
ccmux でも tmux でも VS Code のターミナルでも、ペインを分割していくとこんな形になります:
┌─────────┬──────────────┐│ │ ││ │ pane B ││ pane A │ ││ ├──────────────┤│ │ ││ │ pane C │└─────────┴──────────────┘これを頭の中で「どう管理してるんだろう」と考えると、実は二分木が一番シンプルな答えになります。
木として見る
さっきの図は、こういう木です:
縦分割 (50:50) / \ pane A 横分割 (60:40) / \ pane B pane C- 葉はペインそのもの
- 分岐ノードは分割(どっち向き・どんな比率)
ccmux ではこう書いてます:
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 }), }}木が深くなっていくだけで、他は何も変わらない。
描画 = 領域を再帰で分ける
画面全体の長方形を、葉に行き着くまで再帰的に割っていきます:
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 は、見た目ほど難しい問題ではありません。二分木と再帰が書ければ、誰でも作れる。マルチプレクサを作ってみたい人の最初のハードルは、意外と低いです。