fdehau / tui-rs

Build terminal user interfaces and dashboards using Rust

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Optionally break word and trim trailing white spaces for WordWrapper

Phoenix-Chen opened this issue · comments

Problem

Current implementation of WordWrapper trims trailing white spaces by default and does not allow break word. However, in some situations, I would like to keep the trailing white spaces. See screenshot below:
tui_trim_end_2

Solution

My solution to this problem is add break_word: bool to WordWrapper and Wrap. Then in WordWrapper.next_line check break_word and truncate/extend characters accordingly.

I have implemented above approach at this branch.

Alternatively, I can also make break_word default to false, that would make less impact to current code.

I'll provide a sample script (based on examples/paragraph.rs) you can test with in the comments. (you might need to adjust your terminal size a bit)

Please let me know if this is a feature you would like to include. If so, I'll write some test cases and open an PR.

Here is the sample script you can test with:

use crossterm::{
    event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
    execute,
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use std::{
    error::Error,
    io,
    time::{Duration, Instant},
};
use tui::{
    backend::{Backend, CrosstermBackend},
    layout::{Alignment, Constraint, Direction, Layout},
    style::{Color, Modifier, Style},
    text::{Span, Spans},
    widgets::{Block, Borders, Paragraph, Wrap},
    Frame, Terminal,
};

struct App {
    scroll: u16,
}

impl App {
    fn new() -> App {
        App { scroll: 0 }
    }

    fn on_tick(&mut self) {
        self.scroll += 1;
        self.scroll %= 10;
    }
}

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 tick_rate = Duration::from_millis(250);
    let app = App::new();
    let res = run_app(&mut terminal, app, tick_rate);

    // 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,
    tick_rate: Duration,
) -> io::Result<()> {
    let mut last_tick = Instant::now();
    loop {
        terminal.draw(|f| ui(f, &app))?;

        let timeout = tick_rate
            .checked_sub(last_tick.elapsed())
            .unwrap_or_else(|| Duration::from_secs(0));
        if crossterm::event::poll(timeout)? {
            if let Event::Key(key) = event::read()? {
                if let KeyCode::Char('q') = key.code {
                    return Ok(());
                }
            }
        }
        if last_tick.elapsed() >= tick_rate {
            app.on_tick();
            last_tick = Instant::now();
        }
    }
}

fn ui<B: Backend>(f: &mut Frame<B>, app: &App) {
    let size = f.size();

    let block = Block::default().style(Style::default().bg(Color::White).fg(Color::Black));
    f.render_widget(block, size);

    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(1)
        .constraints(
            [
                Constraint::Percentage(50),
                Constraint::Percentage(50),
            ]
            .as_ref(),
        )
        .split(size);

    let text = vec![
        // Spans::from("This is a line "),
        Spans::from(
            vec![
                Span::styled(
                    "        Current WordWrapper implementation trims trailing spaces by default. See following spaces with black background -> ",
                    Style::default().fg(Color::Red),
                ),
                Span::styled(
                    "                      ",
                    Style::default().bg(Color::Black),
                ),
                Span::styled(
                    " or blue ",
                    Style::default().fg(Color::Red),
                ),
                Span::styled(
                    "       ",
                    Style::default().bg(Color::Blue),
                ),
                Span::styled(
                    " or yellow ",
                    Style::default().fg(Color::Red),
                ),
                Span::styled(
                    "       ",
                    Style::default().bg(Color::Yellow),
                )
            ]
        ),
        Spans::from(
            vec![
                Span::styled(
                    "Current WordWrapper does not break word. ",
                    Style::default().fg(Color::Red),
                ),
                Span::styled(
                    "Supercalifragilisticexpialidocious",
                    Style::default().fg(Color::Black),
                ),
            ]
        ),
    ];

    let create_block = |title| {
        Block::default()
            .borders(Borders::ALL)
            .style(Style::default().bg(Color::White).fg(Color::Black))
            .title(Span::styled(
                title,
                Style::default().add_modifier(Modifier::BOLD),
            ))
    };
    let paragraph = Paragraph::new(text.clone())
        .block(create_block("Current implementation"))
        .wrap(Wrap { trim: false, break_word: false });
    f.render_widget(paragraph, chunks[0]);
    let paragraph = Paragraph::new(text)
        .block(create_block("Implement break_word"))
        .wrap(Wrap { trim: false, break_word: true });
    f.render_widget(paragraph, chunks[1]);
}