Skip to content

Collapse borders in a layout

アプリケーションの一般的なレイアウトは、画面を各ペインの周りに境界線に分割することです。多くの場合、これにより、UI が切断されているようになります。たとえば、次のレイアウト:

problem

次のコードによって作成されました。

fn draw(frame: &mut Frame) {
// create a layout that splits the screen into 2 equal columns and the right column
// into 2 equal rows
let [left, right] = Layout::horizontal([Constraint::Fill(1); 2]).areas(frame.area());
let [top_right, bottom_right] = Layout::vertical([Constraint::Fill(1); 2]).areas(right);
frame.render_widget(Block::bordered().title("Left Block"), left);
frame.render_widget(Block::bordered().title("Top Right Block"), top_right);
frame.render_widget(Block::bordered().title("Bottom Right Block"), bottom_right);
}

しかし、私たちは境界線を崩壊させることでより良くすることができます。例えば。:

solution

最初にする必要があることは、どの境界線が崩壊するかを判断することです。上のレイアウトでは、右下のブロックを中央の垂直境界に接続する必要があるため、右のブロックではなく左上のブロックと左下のブロックでこれをレンダリングする必要があります。

これを達成するにはシンボルモジュールを使用する必要があるため、インポートに追加します。

use ratatui::{
layout::{Constraint, Layout},
symbols,
widgets::{Block, Borders},
DefaultTerminal, Frame,
};

私たちの最初の変更は、右の境界を削除する左ブロックへのものです。

let left_block = Block::new()
// don't render the right border because it will be rendered by the right block
.borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM)
.title("Left Block");

次に、右上のブロックの左上角が左ブロックの右上隅に結合することがわかります。そのため、それをT形に置き換える必要があります。また、右下のブロックでレンダリングされるため、底部の境界線も省略されています。これを実現するために、カスタムsymbols::border::Setを使用します。

// top right block must render the top left border to join with the left block
let top_right_border_set = symbols::border::Set {
top_left: symbols::line::NORMAL.horizontal_down,
..symbols::border::PLAIN
};
let top_right_block = Block::new()
.border_set(top_right_border_set)
// don't render the bottom border because it will be rendered by the bottom block
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
.title("Top Right Block");

右下のブロックでは、右上の角が左のブロックの右の境界に結合していることがわかります。したがって、これを右を指す水平の T 字型に分割する必要があります。右上の角と左下の角についても、同じことを行う必要があります。

// bottom right block must render:
// - top left border to join with the left block and top right block
// - top right border to join with the top right block
// - bottom left border to join with the left block
let collapsed_top_and_left_border_set = symbols::border::Set {
top_left: symbols::line::NORMAL.vertical_right,
top_right: symbols::line::NORMAL.vertical_left,
bottom_left: symbols::line::NORMAL.horizontal_up,
..symbols::border::PLAIN
};
let bottom_right_block = Block::new()
.border_set(collapsed_top_and_left_border_set)
.borders(Borders::ALL)
.title("Bottom Right Block");

最後に、ブロックをレンダリングします。

frame.render_widget(left_block, left);
frame.render_widget(top_right_block, top_right);
frame.render_widget(bottom_right_block, bottom_right);

ここでそのままにしておけば、大部分は問題ありませんが、小さな領域では 50/50 の分割が正しく表示されなくなることに気付くでしょう。これは、デフォルトでは奇数の行または列を 2 つに分割するときに切り上げが行われるためです (例: 5 行 => 2.5/2.5 => 3/2)。通常はこれで問題ありませんが、ブロック間の境界を折りたたむと、最初のブロックには折りたたまれたブロックがないため、すでに 1 行 (または列) が追加されます。この問題は、最後のレイアウト アイテムに少量の追加スペースを割り当てることで簡単に回避できます (例: 49/51 または 33/33/34 を使用)。

// use a 49/51 split instead of 50/50 to ensure that any extra space is on the right
// side of the screen. This is important because the right side of the screen is
// where the borders are collapsed.
let [left, right] =
Layout::horizontal([Constraint::Percentage(49), Constraint::Percentage(51)])
.areas(frame.area());
// use a 49/51 split to ensure that any extra space is on the bottom
let [top_right, bottom_right] =
Layout::vertical([Constraint::Percentage(49), Constraint::Percentage(51)]).areas(right);

この例の完全なコードは、https://github.com/ratatui/ratatui-website/blob/main/code/how-to-collapse-bordersで入手できます

collapse-borders.rs
use std::time::Duration;
use color_eyre::Result;
use crossterm::event::{self, Event};
use ratatui::{
layout::{Constraint, Layout},
symbols,
widgets::{Block, Borders},
DefaultTerminal, Frame,
};
/// This example shows how to use custom borders to collapse borders between widgets.
/// See https://ratatui.rs/how-to/layout/collapse-borders for more info
fn main() -> Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> Result<()> {
loop {
terminal.draw(draw)?;
if key_pressed()? {
return Ok(());
}
}
}
fn key_pressed() -> Result<bool> {
Ok(event::poll(Duration::from_millis(16))? && matches!(event::read()?, Event::Key(_)))
}
fn draw(frame: &mut Frame) {
// create a layout that splits the screen into 2 equal columns and the right column
// into 2 equal rows
// use a 49/51 split instead of 50/50 to ensure that any extra space is on the right
// side of the screen. This is important because the right side of the screen is
// where the borders are collapsed.
let [left, right] =
Layout::horizontal([Constraint::Percentage(49), Constraint::Percentage(51)])
.areas(frame.area());
// use a 49/51 split to ensure that any extra space is on the bottom
let [top_right, bottom_right] =
Layout::vertical([Constraint::Percentage(49), Constraint::Percentage(51)]).areas(right);
let left_block = Block::new()
// don't render the right border because it will be rendered by the right block
.borders(Borders::TOP | Borders::LEFT | Borders::BOTTOM)
.title("Left Block");
// top right block must render the top left border to join with the left block
let top_right_border_set = symbols::border::Set {
top_left: symbols::line::NORMAL.horizontal_down,
..symbols::border::PLAIN
};
let top_right_block = Block::new()
.border_set(top_right_border_set)
// don't render the bottom border because it will be rendered by the bottom block
.borders(Borders::TOP | Borders::LEFT | Borders::RIGHT)
.title("Top Right Block");
// bottom right block must render:
// - top left border to join with the left block and top right block
// - top right border to join with the top right block
// - bottom left border to join with the left block
let collapsed_top_and_left_border_set = symbols::border::Set {
top_left: symbols::line::NORMAL.vertical_right,
top_right: symbols::line::NORMAL.vertical_left,
bottom_left: symbols::line::NORMAL.horizontal_up,
..symbols::border::PLAIN
};
let bottom_right_block = Block::new()
.border_set(collapsed_top_and_left_border_set)
.borders(Borders::ALL)
.title("Bottom Right Block");
frame.render_widget(left_block, left);
frame.render_widget(top_right_block, top_right);
frame.render_widget(bottom_right_block, bottom_right);
}