LoginSignup
13
13

RustのTUIライブラリratatui-rsの基本

Last updated at Posted at 2022-06-19

tui-rs :point_right: GitHubDocs
crossterm :point_right: GitHubDocs
ratatui :point_right:Github, Docs

💡 crosstermのアップデートがあったので記事の内容を動かせるようにした

💡 tui-rsのウィジェットの説明を追加した

💡 tui-rsの代替のratatuiの依存関係に変更した(2023/5/dd)

💡 ratatuiの機能を追加した(2023/5/dd)

入れるクレート

[dependencies]
-- tui = "0.19.0"
++ ratatui = "0.24"
++ crossterm = "0.27"

ratatui

issueにあるように、tui-rsがメンテナンスできないのでフォークがされていた

こちらのほうで熱心に開発が続けられている

気づいていなかった...

crossterm

このcrosstermはバックエンド(ターミナル内の処理)として使われる

これにより色付けやカーソル移動、ターミナルの入力受付などを担当している

重要なのはratatuiのwidgetは必ずBlockが使われるということ

曲線などは作れず必ず、Blockが元になった図形になる

ドットが利用できるようになった

ドット絵で丸などの複雑な図形を描くこともできる

image.png

使うウィジェット類

エラーが出てもそれをimportするだけで良いのであくまでも参考までに

use crossterm::{
    cursor::{MoveDown, MoveLeft, MoveTo, Show},
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType},
};
use ratatui::{
    backend::{Backend, CrosstermBackend},
    layout::{Alignment, Constraint, Direction, Layout},
    style::{Color, Style},
    widgets::{Block, BorderType, Borders},
    Frame, Terminal,
};
use std::io::{self, stdout, Result};

入力系(キー入力のみ)

crosstermのバージョンが0.26からwindowsでは入力が重複するバグあるので注意

なんかいろいPRがって修正されているっぽい?

kindで押した判定を取得すれば重複は発生しない

日本語などのマルチバイト文字を利用する場合は文字数と実際の大きさが一致しないため,位置が右へずれていく(アルファベッド2文字が日本語1文字)

そのため,dependenciesにあるようにuniwidthを使って文字サイズを測ってカーソル位置を移動させる必要がある(ここでは書かない)

以下が基本的なキー入力処理になる

Event::Key(key) = event::read()?

これをif letで受け取ったら一文字の入力になりwhile letにしたら一行の入力になる

バリアントの詳細はcrosstermに載っているため省略するが大雑把説明する

Char('a'), Char('#')などの一文字

Modifier(LeftShift), Modifier(LeftAlt)などの修飾キー

Escなどの固有のキー

このようにenumのバリアントで力入を受け取る

例えば、AltとShift-Aを受け取ったとすると

KeyEvent {
    code: Char('A'),
    modifiers: ALT | SHIFT,
    kind: Press,
    state: NONE,
}

このような形で渡される

上のコードの(key)をの()の中身をKeyEvent{ code, modifier, kind, state }と記述すると、それぞれでマッチングを取ることができる

KeyEvent{code, ..}と書いて他の箇所を省略することもできる

具体的なコードは以下のようになる

struct ButtonDeath {
    x: u16,
    y: u16,
    width: u16,
    height: u16,
}

impl ButtonDeath {
    fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
        Self {
            x,
            y,
            width,
            height,
        }
    }

    fn is_in_button(&self, x: u16, y: u16) -> bool {
        match (
            self.x <= x,
            x <= self.x + self.width,
            self.y <= y,
            y <= self.y + self.height,
        ) {
            (true, true, true, true) => true,
            _ => false,
        }
    }

    fn get_left_top(&self) -> (u16, u16) {
        (self.x, self.y)
    }
}

fn print_events() -> Result<()> {
    let button = ButtonDeath::new(2, 2, 4, 0);
    let (left_x, left_y) = button.get_left_top();
    loop {
        match event::read()? {
            Event::Mouse(MouseEvent {
                kind, column, row, ..
            }) => {
                if button.is_in_button(column, row) {
                    execute!(
                        stdout(),
                        MoveTo(left_x, left_y),
                        Clear(ClearType::CurrentLine),
                        Print("hover".with(Color::Black.into()).on(Color::Blue.into()))
                    )?;
                    match kind {
                        MouseEventKind::Down(MouseButton::Left) => {
                            execute!(
                                stdout(),
                                MoveTo(left_x, left_y + 1),
                                Print("Clicked".with(Color::Green.into()))
                            )?;
                        }
                        MouseEventKind::Up(_) => {
                            execute!(
                                stdout(),
                                MoveTo(left_x, left_y + 1),
                                Clear(ClearType::CurrentLine)
                            )?;
                        }
                        _ => {}
                    }
                } else {
                    execute!(
                        stdout(),
                        MoveTo(left_x, left_y),
                        Clear(ClearType::CurrentLine),
                        Print("Here")
                    )?;
                }
            }
            Event::Key(event) => match event {
                KeyEvent {
                    code: KeyCode::Esc, ..
                } => break,
                _ => {
                    execute!(stdout(), MoveTo(0, 0))?;
                    println!("{:?}", event);
                }
            },
            _ => {}
        }
    }
    Ok(())
}

スクリーンショット 2024-01-29 225342.png

更により具体的なキーにマッチングさせて任意の入力をさせようとすると以下のようにmatchで書ける

これの弱点?としてキー入力がハードコーディングになっているため、ユーザーの設定でいろいろキーコンフィグを変化させたいときには別の処理が必要になる

fn input() -> Result<String> {
    let mut line = String::new();
    let mut stdout = stdout();
    while let Event::Key(KeyEvent { code, kind, .. }) = event::read()? {
        // Enter: 確定
        // Esc: 取り消し
        // others: Inputs
        // BS: 一文字取り消し
        if kind != KeyEventKind::Press {
            continue;
        }
        match code {
            KeyCode::Enter => break,
            KeyCode::Esc => {
                line.clear();
                execute!(
                    stdout,
                    MoveTo(2, 1),
                    Clear(ClearType::CurrentLine),
                    Print(&line)
                )
                .unwrap();
                break;
            }
            KeyCode::Backspace => {
                line.pop();
            }
            KeyCode::Char(c) => line.push(c),
            _ => {}
        }
        execute!(
            stdout,
            MoveTo(1, 1),
            Clear(ClearType::CurrentLine),
            Print(&line)
        )
        .unwrap();
    }
    Ok(line)
}

fn main() {
    let mut stdout = stdout();
    execute!(stdout, MoveTo(1, 1), Clear(ClearType::All), Show).unwrap();
    loop {
        if let Ok(line) = input() {
            if line == ":q" {
                execute!(stdout, Clear(ClearType::All),).unwrap();
                break;
            }
            let line_length = line.chars().count() as u16;
            execute!(
                stdout,
                MoveLeft(line_length), // 文字列分左にずらすがマルチバイトには対応していない
                MoveDown(1),
                Clear(ClearType::CurrentLine),
                Print(format!("{line_length} chars and `{line}`")),
            )
            .unwrap();
        }
    }
}

スクリーンショット 2024-01-30 000623.png

一行だけの入力になるので複数行欲しい場合は別の処理が必要になる

こういった処理でwhile letを見ると代入式は便利だなと思う

ターミナル操作などしてしまうと、入力として受け取られるため文字の後ろから結果が表示されたりする

また冒頭でも説明した通り日本語の文字サイズとアルファベットの文字サイズは違うため日本語を入力すると左にずれるのがたりなくなる

複数入力

ratatui-rsのexamplesと上のを利用する

シンプルな複数入力なため,行をまたいでの削除や確定した入力の取り消しはできなく,メモを保存する機能などもない

ただ,文字列として保持されているので,メソッドを追加すれば消したりFileに保存もできる

GifMaker_20220618193703939.gif

:point_right: SpansがなくなりLineになった
:point_right: Pressedを追加

すこし長めのコード
use crossterm::event::{
    read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind,
    KeyModifiers,
};
use crossterm::execute;
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::{
    backend::{Backend, CrosstermBackend},
    layout::{Constraint, Direction, Layout},
    style::{Color, Modifier, Style},
    text::{Line, Span, Text},
    widgets::{Block, Borders, List, ListItem, Paragraph, Wrap},
    Frame, Terminal,
};

use std::collections::HashMap;
use std::{error::Error, io};

enum InputMode {
    Normal,
    Editing,
}

/// App holds the state of the application
struct App {
    input: String,
    current_with: usize,
    input_width: HashMap<usize, u16>,
    input_mode: InputMode,
    messages: Vec<String>,
    lines: usize,
}

impl App {
    fn count_newline(&self) -> u16 {
        self.input.chars().filter(|&x| x == '\n').count() as u16
    }

    fn set_input_width(&mut self) {
        let lines = self.input_width.get_mut(&self.lines).unwrap();
        *lines = (self.input.chars().count() - self.current_with) as u16;
    }

    fn input_width(&self) -> &u16 {
        let index = self.input_width.get(&self.lines).unwrap();
        index
    }

    fn input_width_init(&mut self) {
        self.input_width = HashMap::from([(0, 0)]);
        self.current_with = 0;
        self.lines = 0;
    }

    fn add_newline(&mut self) {
        self.lines += 1;
        self.set_newline_input_width(self.lines, 0);
        self.set_current_input_width();
    }

    fn set_newline_input_width(&mut self, index: usize, width: u16) {
        self.input_width.insert(index, width);
    }

    fn set_current_input_width(&mut self) {
        self.current_with = self.input.chars().count();
    }
}

impl Default for App {
    fn default() -> App {
        App {
            input: String::new(),
            current_with: 0,
            input_width: HashMap::from([(0, 0)]),
            input_mode: InputMode::Normal,
            messages: Vec::new(),
            lines: 0,
        }
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    // setup terminal
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // create app and run it
    let app = App::default();
    let res = run_app(&mut terminal, app);

    // restore terminal
    disable_raw_mode()?;
    execute!(
        terminal.backend_mut(),
        LeaveAlternateScreen,
        DisableMouseCapture
    )?;
    terminal.show_cursor()?;

    if let Err(err) = res {
        println!("{:?}", err)
    }

    Ok(())
}

fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> io::Result<()> {
    loop {
        // 描画する内容は全てui関数にまかせてる
        terminal.draw(|f| ui(f, &app))?;
        // ここら辺はさきほどのcrosstermの例にあったように入力を受け取った処理をしてる
        if let Event::Key(KeyEvent {
            code,
            modifiers,
            kind,
            ..
        }) = read()?
        {
            match app.input_mode {
                // ノーマルモードはeとesc以外の入力は無視される
                InputMode::Normal => match (code, modifiers, kind) {
                    (KeyCode::Char('e'), KeyModifiers::NONE, KeyEventKind::Press) => {
                        app.input_mode = InputMode::Editing;
                    }
                    (KeyCode::Esc, KeyModifiers::NONE, KeyEventKind::Press) => {
                        return Ok(());
                    }
                    _ => {}
                },
                // 編集モードは入力をappに保存する
                InputMode::Editing => match (code, modifiers, kind) {
                    (KeyCode::Enter, KeyModifiers::CONTROL, KeyEventKind::Press) => {
                        app.messages.push(app.input.drain(..).collect());
                        app.input_width_init();
                    }
                    (KeyCode::Enter, KeyModifiers::NONE, KeyEventKind::Press) => {
                        app.input.push('\n');
                        app.add_newline();
                    }
                    (KeyCode::Char(c), _, KeyEventKind::Press) => {
                        app.input.push(c);
                        app.set_input_width();
                    }
                    (KeyCode::Backspace, KeyModifiers::NONE, KeyEventKind::Press) => {
                        if let Some(last) = app.input.chars().last() {
                            if last != '\n' {
                                app.input.pop();
                                app.set_input_width();
                            }
                        }
                    }
                    (KeyCode::Esc, KeyModifiers::NONE, KeyEventKind::Press) => {
                        app.input_mode = InputMode::Normal;
                    }
                    _ => {}
                },
            }
        }
    }
}

fn ui(f: &mut Frame, app: &App) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(2)
        .constraints(
            [
                // 文字を入力する下の部分と固定の文章を書きこむ部分の二つに分かれている
                Constraint::Length(1),
                Constraint::Percentage(90),
            ]
            .as_ref(),
        )
        .split(f.size());

    let windows = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(
            [
                // 先程の90%分をさらに左右に分割している
                Constraint::Percentage(50),
                Constraint::Percentage(50),
            ]
            .as_ref(),
        )
        .split(chunks[1]);

    let (msg, style) = match app.input_mode {
        // uiでモードをわけているけど、分けて書いた方が良い
        InputMode::Normal => (
            vec![
                Span::raw("Press "),
                Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
                Span::raw(" to exit, "),
                Span::styled("e", Style::default().add_modifier(Modifier::BOLD)),
                Span::raw(" to start editing."),
            ],
            Style::default().add_modifier(Modifier::RAPID_BLINK),
        ),
        InputMode::Editing => (
            vec![
                Span::raw("Press "),
                Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
                Span::raw(" to stop editing, "),
                Span::styled("Ctrl+Enter", Style::default().add_modifier(Modifier::BOLD)),
                Span::raw(" to record the message"),
            ],
            Style::default(),
        ),
    };
    let mut text = Text::from(Line::from(msg));
    text.patch_style(style);
    let help_message = Paragraph::new(text);
    f.render_widget(help_message, chunks[0]);

    let input = Paragraph::new(app.input.clone())
        .style(match app.input_mode {
            InputMode::Normal => Style::default(),
            InputMode::Editing => Style::default().fg(Color::Yellow),
        })
        .block(Block::default().borders(Borders::ALL).title("Input"))
        .wrap(Wrap { trim: false });
    f.render_widget(input, windows[0]);
    match app.input_mode {
        InputMode::Normal => {}
        InputMode::Editing => f.set_cursor(
            windows[0].x + app.input_width() + 1,
            windows[0].y + app.count_newline() + 1,
        ),
    }
    // println!("{:#?}", app.messages);
    let messages: Vec<ListItem> = app
        .messages
        .iter()
        .flat_map(|x| x.split('\n').collect::<Vec<&str>>())
        .enumerate()
        .map(|(i, m)| {
            let content = vec![Line::from(Span::raw(format!("{}: {}", i, m)))];
            ListItem::new(content)
        })
        .collect();
    let messages =
        List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
    f.render_widget(messages, windows[1]);
}

ブロック単体描画

先ほど述べた通り,ratatui-rsでは四角形(Block)が基本的な図形となる

widgetのほぼすべてがこの四角形を作成するのでこれの作り方を覚えていれば応用が利くと思う

qを押すと終了することができる

use crossterm::{
    event::{DisableMouseCapture, EnableMouseCapture},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode},
};
use std::{error::Error, io};
use ratatui::{
    backend::{Backend, CrosstermBackend},
    layout::{Alignment, Constraint, Layout},
    style::{Color, Style},
    widgets::{Block, BorderType, Borders},
    Frame, Terminal,
};

fn main() -> Result<(), Box<dyn Error>> {
    // rawモードの詳細は以下のURL
    // https://docs.rs/crossterm/latest/crossterm/terminal/index.html#raw-mode
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnableMouseCapture)?; // マウスの入力を受け取らない

    // 必ずターミナルインスタンスを生成する必要がある
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // 描画開始
    terminal.draw(ui)?;

    // ターミナルインスタンスの後処理
    disable_raw_mode()?; // rawモードの解除
    execute!(terminal.backend_mut(), DisableMouseCapture)?;
    terminal.show_cursor()?;

    Ok(())
}

fn ui(f: &mut Frame) {
    // ターミナルの大きさ
    let size = f.size();

    // 描画する四角の大きさを求める
    // splitするとスライスが渡されるため今回の一つだけの時もchunk[0]の添え字が必要
    let chunk = Layout::default()
        .constraints([Constraint::Percentage(100)].as_ref()) // 切り出す四角形の制約が一つだけ
        .split(size); // 切り取る基準をターミナルの大きさにする

    // 色を決める時はStyle::default()までが必須になる
    // bgで背景,fgで前景となり,今回使っていないがadd_modifierで太字などの修飾もできる
    // Color::Rgb(u8, u8, u8)でカラーコードで色を追加することもできる
    let brdr_style = Style::default().bg(Color::Cyan).fg(Color::Black); // ボーダーラインの背景と線の両方に色を付けれる
    let blck_style = Style::default().bg(Color::White); // この場合は背景のみで前景(fg)が意味ない
    let block = Block::default() // ウィジェットを使うときはとりあえずdefault()を使う
        .title("Block Title") // String型では代入できないので注意
        .title_alignment(Alignment::Center) // Left, Center, Right
        .borders(Borders::ALL) // これを追加しないと境界線が描画されない
        .border_type(BorderType::Double) // Double, Plain, Round, Thick
        .border_style(brdr_style) // ボーダーラインの色を追加する
        .style(blck_style); // 四角形の色を追加する

    // ウィジェット(Block)と描画する四角形の大きさ(Rect)を入力することで描画される
    // ここの部分を書かないと描画されない
    f.render_widget(block, chunk[0]);
}

スクリーンショット 2023-05-21 140732.png

描画領域の大きさ

tumxやi3wmなどを使ったことがある人ならわかると思うがターミナルを割合で分割していくイメージである

基本的にはターミナルの大きさ(f.size())を基準として縦か横で分割してく

縦か横かはその都度指定が必要で,その上で分割された四角形の大きさの制約を記述してく

use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode},
};
use ratatui::{
    backend::CrosstermBackend,
    layout::{Alignment, Constraint, Direction, Layout},
    style::{Color, Style},
    widgets::{Block, BorderType, Borders},
    Frame, Terminal,
};
use std::{error::Error, io};

fn main() -> Result<(), Box<dyn Error>> {
    // ターミナルの設定
    enable_raw_mode()?;
    let mut stdout = io::stdout();
    execute!(stdout, EnableMouseCapture)?;
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    let mut index = 0;
    // 描画開始
    terminal.draw(ui00)?;
    loop {
        if let Event::Key(key) = event::read()? {
            match (key.code, key.kind) {
                (KeyCode::Char('q'), KeyEventKind::Press) => break,
                (KeyCode::Right, KeyEventKind::Press) => {
                    index = (index + 1) % 4;
                }
                (KeyCode::Left, KeyEventKind::Press) => {
                    index -= 1;
                    if index < 0 {
                        index = 3;
                    }
                }
                _ => {}
            }
        }
        match index {
            0 => terminal.draw(ui00), // ui01と同じ
            1 => terminal.draw(ui01), // ui00とボーダーラインだけ違う
            2 => terminal.draw(ui02), // 左右に分割するときは右に分割されていく
            3 => terminal.draw(ui03), // 上下に分割するときは下に分割されていく
            _ => panic!("Invalid index"),
        }?;
    }

    disable_raw_mode()?;
    execute!(terminal.backend_mut(), DisableMouseCapture)?;
    terminal.show_cursor()?;

    Ok(())
}

fn ui00(f: &mut Frame) {
    // ターミナルの大きさ
    let size = f.size();

    // 描画する四角の大きさを求める
    // splitするとスライスが渡されるため今回の一つだけの時もchunk[0]の添え字が必要
    let chunk = Layout::default()
        .constraints([Constraint::Percentage(100)].as_ref()) // 切り出す四角形の制約が一つだけ
        .split(size); // 切り取る基準をターミナルの大きさにする

    // 色を決める時はStyle::default()までが必須になる
    // Color::Rgb(u8, u8, u8)でカラーコードで色を追加することもできる
    let brdr_style = Style::default().bg(Color::Cyan).fg(Color::Black); // ボーダーラインの背景と線の両方に色を付けれる
    let blck_style = Style::default().bg(Color::White); // この場合は背景のみで前景(fg)は意味ない
    let block = Block::default() // ウィジェットを使うときはとりあえずはこれをつかう
        .title("ui00") // String型では代入できないので注意
        .title_alignment(Alignment::Center) // Left, Center, Right
        .borders(Borders::ALL) // これを追加しないとタイトルや境界線が描画されない
        .border_type(BorderType::Double) //
        .border_style(brdr_style) // ボーダーラインの色を追加する
        .style(blck_style); // 四角形の色を追加する

    f.render_widget(block, chunk[0]); // renderでウィジェット(Block)と描画する四角形の大きさ(Rect)を入力
}

fn ui01(f: &mut Frame) {
    let chunks = Layout::default()
        .direction(Direction::Vertical) // 上下分割
        .constraints(
            [
                // ここが10や50,100でも変わらない
                Constraint::Percentage(10), // 他に大きさの制約を指定していないから
            ]
            .as_ref(),
        )
        .split(f.size());
    let brdr_style = Style::default().bg(Color::Cyan).fg(Color::Black);
    let blck_style = Style::default().bg(Color::White);

    let block = Block::default()
        .title("ui01")
        .title_alignment(Alignment::Center)
        .borders(Borders::ALL)
        .border_type(BorderType::Plain)
        .border_style(brdr_style)
        .style(blck_style);

    f.render_widget(block, chunks[0]);
}

fn ui02(f: &mut Frame) {
    let chunks = Layout::default()
        .direction(Direction::Horizontal) // 左右分割
        .constraints(
            [
                Constraint::Percentage(10), // ここが描画する制約になる
                Constraint::Percentage(10), // 1対9で表示される
            ]
            .as_ref(),
        )
        .split(f.size());
    let brdr_style = Style::default().bg(Color::White).fg(Color::Black);
    let blck_style = Style::default().bg(Color::Cyan);

    let block = Block::default()
        .title("ui02")
        .title_alignment(Alignment::Center)
        .borders(Borders::ALL)
        .border_type(BorderType::Thick)
        .border_style(brdr_style)
        .style(blck_style);

    f.render_widget(block.clone(), chunks[0]); // renderされるとwidgetが消費される
    f.render_widget(block, chunks[1]);
}
fn ui03(f: &mut Frame) {
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints(
            [
                Constraint::Percentage(10), // ここが描画する制約になる
                Constraint::Percentage(10), // ここも描画する制約になる
                Constraint::Percentage(10), // 1対1対8で表示される
            ]
            .as_ref(),
        )
        .split(f.size());
    let brdr_style = Style::default().bg(Color::Cyan).fg(Color::Black);
    let blck_style = Style::default().bg(Color::White);

    let block = Block::default()
        .title("ui03")
        .title_alignment(Alignment::Center)
        .borders(Borders::ALL)
        .border_type(BorderType::Rounded)
        .border_style(brdr_style)
        .style(blck_style);

    f.render_widget(block.clone(), chunks[0]);
    f.render_widget(block.clone(), chunks[1]);
    f.render_widget(block, chunks[2]);
}

これは推測なので申し訳ないが,これは制約解決アルゴリズム?(cassoway.rs)の充足問題を解決するように画面が描画されるかららしい

helix-editorもこれを使って画面の大きさの指定をしている

最終的にそれら切り分けられた四角形をrender()に代入するかどうかで描画するかしないかを決める

コードのf.render_widget()の箇所をコメントアウトするとその箇所の描画がされなくなる

fn ui(f: &mut Frame) {
    let chunks = Layout::default()
        .direction(Direction::Horizontal)
        .constraints(
            [
                Constraint::Percentage(33),
                Constraint::Percentage(33),
                Constraint::Percentage(33),
            ]
            .as_ref(),
        )
        .split(f.size());

    let left_block = Paragraph::new("Block01")
        .block(Block::default().borders(Borders::ALL))
        .alignment(tui::layout::Alignment::Left);
    f.render_widget(left_block, chunks[0]);

    let middle_chunks = Layout::default()
        .direction(Direction::Vertical)
        .constraints(
            [
                Constraint::Percentage(33),
                Constraint::Percentage(33),
                Constraint::Percentage(33),
            ]
            .as_ref(),
        )
        .split(chunks[1]);
    let middle_top_block = Paragraph::new("Middle01")
        .block(Block::default().borders(Borders::ALL))
        .alignment(tui::layout::Alignment::Center);
    f.render_widget(middle_top_block, middle_chunks[0]);

    let center_block = Paragraph::new("Middle02")
        .block(Block::default().borders(Borders::ALL))
        .alignment(tui::layout::Alignment::Center);
    f.render_widget(center_block, middle_chunks[1]);

    let middle_bottom = Paragraph::new("Middle03")
        .block(Block::default().borders(Borders::ALL))
        .alignment(tui::layout::Alignment::Center);
    f.render_widget(middle_bottom, middle_chunks[2]);

    let right_block = Paragraph::new("Block02")
        .block(Block::default().borders(Borders::ALL))
        .alignment(tui::layout::Alignment::Right);
    f.render_widget(right_block, chunks[2]);
}

NuShell 2022_06_08 18_45_52.png

popupの例があるようにターミナル内の大きさであることが保証されていればRect(x, y, width, height)を指定すれば四角形を任意の場所と大きさで作成することができる

もちろん大きすぎるとパニックを起こし、埋まっていれば描画されない

なので,f.size()のwidthとheightの割合を使ってターミナル内に収まるようにしたり,描画エリアを消すなどの工夫をする必要がある

例えば左上(0, 0)からx軸に10,y軸10,幅20,高さ20で描画を消す

恐らくこの数値は文字の縦幅と横幅を利用していると思われる

したがって,Constraints::Length()と同じだと思う(確認できていない)

    f.render_widget(
        Clear,
        Rect {
            x: 10, 
            y: 10,
            width: 20, 
            height: 20,
        },
    )

左下の線がターミナルの枠よりもはみ出た時点でpanicを起こす

なので,ターミナルが小さければそもそも起動できなかったりする

NuShell 2022_06_15 20_51_55.png

テキストの整列

テキストはLayoutのalignmentを持つParagraph,titleくらいしか寄せることができないListやTableなどは内部のテキストを整列させられない

なので,テキストの整列が必要な場合はLayoutを細かく分割し,その中でParagraphやList,Tableを使うのが一番楽にできる

先ほどの分割したブロック内に日本語や英語のテキストを代入する

fn ui<B: Backend>(f: &mut Frame<B>) {
            let chunks = Layout::default()
                .direction(Direction::Horizontal)
                .constraints(
                    [
                        Constraint::Percentage(33),
                        Constraint::Percentage(33),
                        Constraint::Percentage(33),
                    ]
                    .as_ref(),
                )
                .split(chunks[1]);

            let left_block_text = "このように日本語も表示される\n'\\n'を入れれば改行される\nwrapすれば改行をせずに一文で表示できる\n長すぎると見切れるるうううううう";
            let left_block = Paragraph::new(left_block_text)
                .block(Block::default().borders(Borders::ALL))
                .alignment(tui::layout::Alignment::Left);
            f.render_widget(left_block, chunks[0]);

            let middle_chunks = Layout::default()
                .direction(Direction::Vertical)
                .constraints(
                    [
                        Constraint::Percentage(33),
                        Constraint::Percentage(33),
                        Constraint::Percentage(33),
                    ]
                    .as_ref(),
                )
                .split(chunks[1]);
            let middle_block_text =
                "This is a sentence.\ntomorrow and tomorrow and tomorrow\nThis is a 🖊.";
            let middle_top_block = Paragraph::new(middle_block_text)
                .block(Block::default().borders(Borders::ALL))
                .alignment(tui::layout::Alignment::Center);
            f.render_widget(middle_top_block, middle_chunks[0]);

            let center_block = Paragraph::new(middle_block_text)
                .block(Block::default().borders(Borders::ALL))
                .alignment(tui::layout::Alignment::Center);
            // f.render_widget(center_block, middle_chunks[1]);

            let middle_bottom = Paragraph::new(middle_block_text)
                .block(Block::default().borders(Borders::ALL))
                .alignment(tui::layout::Alignment::Center);
            f.render_widget(middle_bottom, middle_chunks[2]);

            let right_block_text =
                "This is a right block's text.\n Do you see me?\nToday is the day";
            let right_block = Paragraph::new(right_block_text)
                .block(Block::default().borders(Borders::ALL))
                .alignment(tui::layout::Alignment::Right);
            f.render_widget(right_block, chunks[2]);
}

整列された文字.png

余談

私はwindowsでwindows terminalを,linuxではweztermやurxvtを使っている

その折,linuxでtui-rsで指定した色がくすんで見える現象に遭遇した

左上: cmd,右上:vscodeのpowershell,左下:window terminal,右下:powershell

ターミナルごとに違う色.png

いろいろ検索してみたらよくあることらしく,urxvtはそもそもtrue colorじゃなかったりした

テーマを作るのは大変なんだなあと思った

色合いやデザインにこだわりがある人はこのような問題で大変そうだと思った

ちなみに私はある程度色合いが違っていても,好みであれば仔細は問題はないと思っている

    let default_colors = [
        Color::Black,
        Color::Blue,
        Color::Cyan,
        Color::DarkGray,
        Color::Gray,
        Color::Green,
        Color::LightBlue,
        Color::LightCyan,
        Color::LightGreen,
        Color::LightMagenta,
        Color::LightRed,
        Color::LightYellow,
        Color::Magenta,
        Color::Red,
        Color::White,
    ];

fn ui<B: Backend>(f: &mut Frame<B>, colors: [Color; 15]) {
    let size = f.size();
    let mut colors_iter = colors.into_iter().cycle();//5*3以上でも表示できる
    let w = 5;
    let h = 3;
    let wx = size.width / w;
    let hy = size.height / h;
    for i in 0..w {
        for j in 0..h {
            let color = colors_iter.next().unwrap();
            let default_block = Block::default()
                .borders(Borders::ALL)
                .style(Style::default().bg(color).fg(color));
            let rect = Rect {
                x: i * wx,
                y: j * hy,
                width: wx,
                height: hy,
            };
            f.render_widget(default_block, rect);
        }
    }
}
13
13
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
13
13