3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

よりそうAdvent Calendar 2023

Day 24

ratatuiのチュートリアルでマルチスレッド処理を学ぶ

Last updated at Posted at 2023-12-25

はじめに

ratatuiのチュートリアルを通して
TUIを作りながら学べたことをまとめた記事になります。

環境

  • cargo 1.72.1

成果物

今回はチュートリアルのCounter Appを作ってみました。
シンプルながらもRust初心者にもわかりやすい設計になっており、学び甲斐があります。

Videotogif (2).gif

学んだこと

スレッドの生成とイベントの監視

EventHandlerのnew関数では、新しいスレッドを生成してターミナルイベントを非同期に監視します。

thread::spawnを使用して新しいスレッドを生成し、無限ループ内でイベントをポーリングしています。

impl EventHandler {
    pub fn new(tick_rate: u64) -> Self {
        let tick_rate = Duration::from_millis(tick_rate);
        let (sender, receiver) = mpsc::channel();
        let handler = {
            let sender = sender.clone();
            thread::spawn(move || {
                let mut last_tick = Instant::now();
                loop {
                    let timeout = tick_rate
                        .checked_sub(last_tick.elapsed())
                        .unwrap_or(tick_rate);

                    if event::poll(timeout).expect("unable to poll events") {
                        match event::read().expect("unable to read event") {
                            CrosstermEvent::Key(e) => {
                                if e.kind == event::KeyEventKind::Press {
                                    sender.send(Event::Key(e))
                                } else {
                                    Ok(())
                                }
                            }
                            CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)),
                            CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)),
                            _ => unimplemented!(),
                        }
                        .expect("failed to send terminal event")
                    }
                    if last_tick.elapsed() >= tick_rate {
                        sender.send(Event::Tick).expect("failed to send tick event");
                        last_tick = Instant::now();
                    }
                }
            })
        };
        Self {
            sender,
            receiver,
            handler,
        }
    }

生成されたスレッドは、tick_rateに基づいて定期的にTickイベントを生成し、ターミナルからのイベントをリアルタイムで処理します。

マルチスレッド処理

EventHandlerが新しいスレッドを生成することでイベント処理(キー入力やマウスイベント、ウィンドウのサイズ変更など)を非同期に監視しています。

メインスレッドはUIの描画や他のタスクに専念でき、アプリケーションの応答性が向上します。

チャネルを用いた通信

mpscチャネルを使用して、生成されたスレッドからメインスレッドへ安全にイベントを送信しています。

let (sender, receiver) = mpsc::channel();

このチャネルを通じて、イベントが効率的にメインスレッドに送られ、アプリケーションの状態が適切に更新されます。

イベント処理のループ

メインスレッドでは、生成されたスレッドから送られてくるイベントをreceiverを通じて受け取り、それに応じてアプリケーションの状態を更新します。

pub fn next(&self) -> Result<Event> {
    Ok(self.receiver.recv()?)
}

イベント駆動アーキテクチャ

#[derive(Clone, Copy, Debug)]
pub enum Event {
    Tick,
    Key(KeyEvent),
    Mouse(MouseEvent),
    Resize(u16, u16),
}

Event enumでは

  • タイマーイベント(Tick)
  • キーボードイベント(Key)
  • マウスイベント(Mouse)
  • ウィンドウリサイズイベント(Resize)

を表しています。
これによって異なるタイプのイベントを簡単に識別し、適切に反応することができます。

イベントループ

メインループはEventHandlerからイベントを受け取り、それに応じてアプリケーションの状態を更新します。

while !app.should_quit {
    tui.draw(&mut app)?;
    match tui.events.next()? {
        Event::Tick => {},
        Event::Key(key_event) => update(&mut app, key_event),
        Event::Mouse(_) => {},
        Event::Resize(_, _) => {},
    }
}

このループでは、アプリケーションが終了するまで、継続的にイベントを監視し、それぞれのイベントタイプに応じた処理を行います。

今回はキーボードイベントのみ実装しているためキー入力が発生したらupdate関数が呼び出され、アプリケーションの状態が更新されます。

UIのカスタマイズのしやすさ

pub fn render(app: &mut App, f: &mut Frame) {
    f.render_widget(
        Paragraph::new(format!(
            "Press `Esc`, `Ctrl-C` or `q` to stop running. \n\
             Press `j` and `k` to increment and decrement the counter respectively. \n\
             Counter: {}",
            app.counter
        ))
        .block(
            Block::default()
                .title("Counter App")
                .title_alignment(Alignment::Center)
                .borders(Borders::ALL)
                .border_type(BorderType::Rounded),
        )
        .style(Style::default().fg(Color::Yellow))
        .alignment(Alignment::Center),
        f.size(),
    )
}

ここではParagraphウィジェットを使用してテキストを表示し、それをBlockで囲んでいます。

Blockにはタイトル、ボーダーのスタイル、タイトルの配置などを設定し、Paragraphウィジェット自体には、黄色の前景色と中央揃えのスタイルが適用されています。

直感的でわかりやすい作りになっています。

感想

ドキュメントが丁寧に作られており、シンプルな構造から機能ごとにリファクタリングする手順まであります。
Rust初心者の自分にもとてもわかりやすくまとまっている印象でした。

次はratatuiで何か作ったら記事にしようと思います。

3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?