はじめに
巷の TUI でいくつかの操作を切り替える際には、パネルの切り替えが存在する。
特に僕がよく使用する LazyGit は、[1] [2] [3] といったキー切り替えが存在する。
Ratatui を使用した際に、これをどう Rust で実装するか気になったので、それをまとめる。
サンプルコード
基本的な考え方
基本的には「フォーカスステートを実装する」という事だけ。
より複雑な場合は、ウィジェットごとにフォーカスステートを持たせて探索させるというのもありかも知れない。
enum
で FocusArea
ステートを作成する
src/app.rs
#[derive(PartialEq)] // 後続の条件比較で使用するので、つけておく
enum FocusArea {
Area1,
Area2,
Area3,
Area4,
Area5,
}
ビューを作る
ビュー側では、単にボーダー側の fg
スタイルを条件に応じて切り分けるという事をするだけ。
src/app.rs
const FOCUSED_COLOR: Color = Color::Green;
const UNFOCUSED_COLOR: Color = Color::White;
// Widget 実装
impl Widget for &App {
fn render(self, area: Rect, buf: &mut Buffer) {
let main_layout =
Layout::horizontal([Constraint::Percentage(40), Constraint::Percentage(60)]);
let [left_area, right_area] = main_layout.areas(area);
let left_area_layout = Layout::vertical([
Constraint::Percentage(20),
Constraint::Percentage(40),
Constraint::Percentage(40),
]);
let [area1_area, area2_area, area3_area] = left_area_layout.areas(left_area);
let right_area_layout =
Layout::vertical([Constraint::Percentage(80), Constraint::Percentage(20)]);
let [area4_area, area5_area] = right_area_layout.areas(right_area);
Paragraph::new("Area 1")
.block(self.create_borders(FocusArea::Area1).title("[1] Area 1"))
.render(area1_area, buf);
// Area 2
Paragraph::new("Area 2")
.block(self.create_borders(FocusArea::Area2).title("[2] Area 2"))
.render(area2_area, buf);
// Area 3
Paragraph::new("Area 3")
.block(self.create_borders(FocusArea::Area3).title("[3] Area 3"))
.render(area3_area, buf);
// Area 4
Paragraph::new("Area 4")
.block(self.create_borders(FocusArea::Area4).title("[4] Area 4"))
.render(area4_area, buf);
// Area 5
Paragraph::new("Area 5")
.block(self.create_borders(FocusArea::Area5).title("[5] Area 5"))
.render(area5_area, buf);
}
}
// Widget レンダリング周りのヘルパ関数実装
impl App {
fn create_borders(&self, focus_at: FocusArea) -> Block<'_> {
let border_color = if self.focus_area == focus_at {
FOCUSED_COLOR
} else {
UNFOCUSED_COLOR
};
Block::bordered()
.borders(Borders::ALL)
.border_set(border::ROUNDED)
.style(Style::default().fg(border_color))
}
}
キーマップを作る
src/app.rs
// メインロジックの実装
impl App {
// -- 省略 --
fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Char('q') => self.quit = true,
KeyCode::Char('1') => self.focus_area = FocusArea::Area1,
KeyCode::Char('2') => self.focus_area = FocusArea::Area2,
KeyCode::Char('3') => self.focus_area = FocusArea::Area3,
KeyCode::Char('4') => self.focus_area = FocusArea::Area4,
KeyCode::Char('5') => self.focus_area = FocusArea::Area5,
_ => {}
}
}
}
あとは必要に応じて機能を実装していく感じ。
例えば、以下の例は n
キーを押せば、1->5↺ と遷移するのを実現している。
enum FocusArea
に next()
を実装して、それを利用する感じ。
src/app.rs
#[derive(PartialEq)]
enum FocusArea {
Area1,
Area2,
Area3,
Area4,
Area5,
}
+// enum FocusArea に対して実装
+impl FocusArea {
+ // 現在の状態から「次」に切り替える関数
+ fn next(&self) -> Self {
+ match self {
+ FocusArea::Area1 => FocusArea::Area2,
+ FocusArea::Area2 => FocusArea::Area3,
+ FocusArea::Area3 => FocusArea::Area4,
+ FocusArea::Area4 => FocusArea::Area5,
+ FocusArea::Area5 => FocusArea::Area1,
+ }
+ }
+}
src/app.rs
impl App {
// -- 省略 --
fn handle_key_event(&mut self, key_event: KeyEvent) {
match key_event.code {
KeyCode::Char('q') => self.quit = true,
KeyCode::Char('1') => self.focus_area = FocusArea::Area1,
KeyCode::Char('2') => self.focus_area = FocusArea::Area2,
KeyCode::Char('3') => self.focus_area = FocusArea::Area3,
KeyCode::Char('4') => self.focus_area = FocusArea::Area4,
KeyCode::Char('5') => self.focus_area = FocusArea::Area5,
+ KeyCode::Char('n') => self.focus_area = self.focus_area.next(),
_ => {}
}
}
}
まとめ
今回は enum
によるスタイル変更でシンプルに対応する例を作成してみた。
enum
での管理は、さくっと実装する分にはかなり楽に、理解しやすく実装できるのでいい。
より Widget が画面内に多く存在していたり、複雑に交差する場合は、Widget 自体に is_focused
ステータスを持たせて、それを管理するといったことをやるといいのかもしれない。
そうした場合の例はまた今度。