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

# はじめに

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

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

![output-palette.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51857/5064194d-d6b8-48fb-9c01-a1a8e08f683e.gif)

# サンプルコード

https://github.com/takavfx/learn_ratatui/tree/dfe93463918c1dccb6aec288a6bee6b20812a3de/examples/focus_area

# 基本的な考え方

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

## `enum` で `FocusArea` ステートを作成する

```rust:src/app.rs
#[derive(PartialEq)] // 後続の条件比較で使用するので、つけておく
enum FocusArea {
    Area1,
    Area2,
    Area3,
    Area4,
    Area5,
}
```

## ビューを作る

ビュー側では、単にボーダー側の `fg` スタイルを条件に応じて切り分けるという事をするだけ。

```rust: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))
    }
}
```

## キーマップを作る

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

```diff_rust: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,
+        }
+    }
+}
```

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