1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ratatui でエリアフォーカスを実装する: enum 管理編

Last updated at Posted at 2025-04-05

はじめに

巷の TUI でいくつかの操作を切り替える際には、パネルの切り替えが存在する。
特に僕がよく使用する LazyGit は、[1] [2] [3] といったキー切り替えが存在する。

Ratatui を使用した際に、これをどう Rust で実装するか気になったので、それをまとめる。

output-palette.gif

サンプルコード

基本的な考え方

基本的には「フォーカスステートを実装する」という事だけ。
より複雑な場合は、ウィジェットごとにフォーカスステートを持たせて探索させるというのもありかも知れない。

enumFocusArea ステートを作成する

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 FocusAreanext() を実装して、それを利用する感じ。

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 ステータスを持たせて、それを管理するといったことをやるといいのかもしれない。
そうした場合の例はまた今度。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?