入力部分の処理は大まかな骨組みができたと思いますので次は画面表示について進めていきたいと思います。
crosstermでスクラッチから表示処理を構築していくのもよいですが、便利なクレートを利用して手間を減らしたいと思います。
そこでクレートを探したところ ratatuiが良さそうでしたのでこちらを導入してみたいと思います。
ratatuiは画面表示をリッチにするためのWidget機能などを提供しバックエンドとしてcrosstermを使用することが可能です1。
導入
crosstermと同様にcargo addコマンドでクレートを追加します。
執筆時点ではv0.26.3がインストールされました。
cargo add ratatui
ratatuiについて
ratautiの説明文によるとこのクレートは immediate mode で動作するlightweight なgraphics libraryです。
表示部分の機能強化を担うcrosstermへのアドオン的なものとなります。
イベント処理などはしないのでその部分は従来通りcrosstermの機能を使用することになります。
ratautiの提供するwidgetを使うとリッチな表現を手軽に使うことができます。
主要な機能
まずはお試しとしてratautiのチュートリアルを元にコードに新たにrender_main() を追加してどのように使うのか動作確認をしてみました。
おそらくコード量が増えると思いますので後にモジュール化することになるでしょう。
今回追加したコードを元にratautiの主要な機能を解説してみたいと思います。
Layout
widgetの描画は四角い領域を指定して出力していく形になります。そのため、まずはどこの座標にどのくらいの大きさで出力するのかを決める必要があります。
座標軸は左上を原点にしていますが画面サイズは端末によって違いますしリサイズも発生します。そのため、都度サイズ割り出しが必要になります。
下記はレイアウト計算をしている箇所になります。
ここでは画面全体のサイズを左右で二分割し、分割された右側の領域を更に上下に二分割して、3つの領域を作っています。
それぞれ3つの領域の最大サイズをそれぞれのwidgetのサイズとして使用することにしています。
// ratatui
let _ = terminal.draw(|frame| {
// 左右に50%分割
let layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.split(frame.size());
// 右側を上下に50%分割
let sub_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(layout[1]);
let main_panel = layout[0];
let sub_panel_0 = sub_layout[0];
let sub_panel_1 = sub_layout[1];
Widget
ratautiは様々なWidgetを提供していますが、ここでは基本的なBlock widgetとParagraph widgetを使用しています。
Block widgetはベーシックなwidgetで四角い領域を表示します。ここでは領域に沿って枠線とタイトルを上下に表示するようにしています。
Paragraph widgetは文字出力に使います。文字の色を変更したり、右寄せなどアライメント機能を提供しています。
// メインパネル
// 上タイトル
let title = Title::from(" main ".bold());
// 下タイトル
let instructions = Title::from(Line::from(vec![" Quit ".into(), "<Ctrl+Q> ".blue().bold()]));
// パネルブロック
let block = Block::default()
.title(title.alignment(Alignment::Center))
.title(
instructions
.alignment(Alignment::Center)
.position(Position::Bottom),
)
.borders(Borders::ALL)
.border_set(border::THICK);
// 16進数ヘッダー
let header = Line::from(format!(
"{:width$} +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F",
" ",
width = 8
));
let contents = Paragraph::new(Text::from(vec![header])).block(block);
Rendering
レンダリング部分はレイアウト部分の続きのコードで実施しています。
レイアウト処理で割り出したサイズとwidgetをrender_widgetに渡して描画、となります。
動作確認のため、同じwidgetを使いまわしています。
// パネルを描画
frame.render_widget(&contents, main_panel);
frame.render_widget(&contents, sub_panel_0);
frame.render_widget(&contents, sub_panel_1);
最終的な画面出力は下記のようになりました。
想定通り3ペインの画面となっています。左側をエディターのメイン画面、右側をASCII表現で表示する領域にしていこうと思いますがまだ詳細は決めていません。
今後はこのコードを調整してエディターらしい画面に仕上げていきたいと思います。
- crosstermの他にもtermionとtermwizをサポートしています。 ↩︎
TOC
GitHubにコードをアップロードしています。
コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。
# Example
$ cargo run --examples first_step