refactor(widgets): replace text rendering in Paragraph

* remove custom markup language
* add Text container for both raw and styled strings
This commit is contained in:
Florian Dehau 2018-08-12 22:13:32 +02:00
parent 7181970a32
commit ad602a54bf
9 changed files with 93 additions and 223 deletions

View File

@ -21,6 +21,7 @@ bitflags = "1.0.1"
cassowary = "0.3.0" cassowary = "0.3.0"
itertools = "0.7.8" itertools = "0.7.8"
log = "0.4.1" log = "0.4.1"
either = "1.5.0"
unicode-segmentation = "1.2.0" unicode-segmentation = "1.2.0"
unicode-width = "0.1.4" unicode-width = "0.1.4"
termion = { version = "1.5.1", optional = true } termion = { version = "1.5.1", optional = true }

View File

@ -20,7 +20,7 @@ use tui::style::{Color, Modifier, Style};
use tui::widgets::canvas::{Canvas, Line, Map, MapResolution}; use tui::widgets::canvas::{Canvas, Line, Map, MapResolution};
use tui::widgets::{ use tui::widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, Item, List, Marker, Paragraph, Row, Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, Item, List, Marker, Paragraph, Row,
SelectableList, Sparkline, Table, Tabs, Widget, SelectableList, Sparkline, Table, Tabs, Widget, Text,
}; };
use tui::{Frame, Terminal}; use tui::{Frame, Terminal};
@ -194,7 +194,7 @@ fn main() {
let tx = tx.clone(); let tx = tx.clone();
loop { loop {
tx.send(Event::Tick).unwrap(); tx.send(Event::Tick).unwrap();
thread::sleep(time::Duration::from_millis(200)); thread::sleep(time::Duration::from_millis(16));
} }
}); });
@ -440,7 +440,24 @@ fn draw_charts(f: &mut Frame<MouseBackend>, app: &App, area: &Rect) {
} }
fn draw_text(f: &mut Frame<MouseBackend>, area: &Rect) { fn draw_text(f: &mut Frame<MouseBackend>, area: &Rect) {
Paragraph::default() let text = [
Text::Data("This is a paragraph with several lines. You can change style your text the way you want.\n\nFox example: "),
Text::StyledData("under", Style::default().fg(Color::Red)),
Text::Data(" "),
Text::StyledData("the", Style::default().fg(Color::Green)),
Text::Data(" "),
Text::StyledData("rainbow", Style::default().fg(Color::Blue)),
Text::Data(".\nOh and if you didn't "),
Text::StyledData("notice", Style::default().modifier(Modifier::Italic)),
Text::Data(" you can "),
Text::StyledData("automatically", Style::default().modifier(Modifier::Bold)),
Text::Data(" "),
Text::StyledData("wrap", Style::default().modifier(Modifier::Invert)),
Text::Data(" your "),
Text::StyledData("text", Style::default().modifier(Modifier::Underline)),
Text::Data(".\nOne more thing is that it should display unicode characters: 10€")
];
Paragraph::new(text.iter())
.block( .block(
Block::default() Block::default()
.borders(Borders::ALL) .borders(Borders::ALL)
@ -448,17 +465,6 @@ fn draw_text(f: &mut Frame<MouseBackend>, area: &Rect) {
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)), .title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)),
) )
.wrap(true) .wrap(true)
.text(
"This is a paragraph with several lines.\nYou can change the color.\nUse \
\\{fg=[color];bg=[color];mod=[modifier] [text]} to highlight the text with a color. \
For example, {fg=red u}{fg=green n}{fg=yellow d}{fg=magenta e}{fg=cyan r} \
{fg=gray t}{fg=light_gray h}{fg=light_red e} {fg=light_green r}{fg=light_yellow a} \
{fg=light_magenta i}{fg=light_cyan n}{fg=white b}{fg=red o}{fg=green w}.\n\
Oh, and if you didn't {mod=italic notice} you can {mod=bold automatically} \
{mod=invert wrap} your {mod=underline text} =).\nOne more thing is that \
it should display unicode characters properly: , ٩(-̮̮̃-̃)۶ ٩(̮̮̃̃)۶ ٩(̯͡͡)۶ \
٩(-̮̮̃̃).",
)
.render(f, area); .render(f, area);
} }

View File

@ -6,9 +6,9 @@ use termion::event;
use termion::input::TermRead; use termion::input::TermRead;
use tui::backend::MouseBackend; use tui::backend::MouseBackend;
use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use tui::style::{Alignment, Color, Style}; use tui::style::{Color, Style, Modifier};
use tui::widgets::{Block, Paragraph, Widget}; use tui::widgets::{Block, Paragraph, Widget, Text};
use tui::Terminal; use tui::Terminal;
fn main() { fn main() {
@ -55,38 +55,24 @@ fn draw(t: &mut Terminal<MouseBackend>, size: &Rect) {
) )
.split(size); .split(size);
Paragraph::default() let text = [
.alignment(Alignment::Left) Text::Data("This a line\n"),
.text( Text::StyledData("This a line\n", Style::default().fg(Color::Red)),
"This is a line\n{fg=red This is a line}\n{bg=red This is a \ Text::StyledData("This a line\n", Style::default().bg(Color::Blue)),
line}\n{mod=italic This is a line}\n{mod=bold This is a \ Text::StyledData("This a longer line\n", Style::default().modifier(Modifier::CrossedOut)),
line}\n{mod=crossed_out This is a line}\n{mod=invert This is a \ Text::StyledData("This a line\n", Style::default().fg(Color::Green).modifier(Modifier::Italic)),
line}\n{mod=underline This is a \ ];
line}\n{bg=green;fg=yellow;mod=italic This is a line}\n",
)
.render(&mut f, &chunks[0]);
Paragraph::default() Paragraph::new(text.iter())
.alignment(Alignment::Left)
.render(&mut f, &chunks[0]);
Paragraph::new(text.iter())
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(true) .wrap(true)
.text(
"This is a line\n{fg=red This is a line}\n{bg=red This is a \
line}\n{mod=italic This is a line}\n{mod=bold This is a \
line}\n{mod=crossed_out This is a line}\n{mod=invert This is a \
line}\n{mod=underline This is a \
line}\n{bg=green;fg=yellow;mod=italic This is a line}\n",
)
.render(&mut f, &chunks[1]); .render(&mut f, &chunks[1]);
Paragraph::default() Paragraph::new(text.iter())
.alignment(Alignment::Right) .alignment(Alignment::Right)
.wrap(true) .wrap(true)
.text(
"This is a line\n{fg=red This is a line}\n{bg=red This is a \
line}\n{mod=italic This is a line}\n{mod=bold This is a \
line}\n{mod=crossed_out This is a line}\n{mod=invert This is a \
line}\n{mod=underline This is a \
line}\n{bg=green;fg=yellow;mod=italic This is a line}\n",
)
.render(&mut f, &chunks[2]); .render(&mut f, &chunks[2]);
} }
t.draw().unwrap(); t.draw().unwrap();

View File

@ -22,7 +22,7 @@ use termion::input::TermRead;
use tui::backend::MouseBackend; use tui::backend::MouseBackend;
use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::{Color, Style}; use tui::style::{Color, Style};
use tui::widgets::{Block, Borders, Item, List, Paragraph, Widget}; use tui::widgets::{Block, Borders, Item, List, Paragraph, Widget, Text};
use tui::Terminal; use tui::Terminal;
struct App { struct App {
@ -115,10 +115,9 @@ fn draw(t: &mut Terminal<MouseBackend>, app: &App) {
.margin(2) .margin(2)
.constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref())
.split(&app.size); .split(&app.size);
Paragraph::default() Paragraph::new([Text::Data(&app.input)].iter())
.style(Style::default().fg(Color::Yellow)) .style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input")) .block(Block::default().borders(Borders::ALL).title("Input"))
.text(&app.input)
.render(&mut f, &chunks[0]); .render(&mut f, &chunks[0]);
List::new( List::new(
app.messages app.messages

View File

@ -29,6 +29,13 @@ pub enum Constraint {
Min(u16), Min(u16),
} }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Left,
Center,
Right,
}
// TODO: enforce constraints size once const generics has landed // TODO: enforce constraints size once const generics has landed
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Layout { pub struct Layout {

View File

@ -167,6 +167,7 @@
extern crate bitflags; extern crate bitflags;
extern crate cassowary; extern crate cassowary;
extern crate itertools; extern crate itertools;
extern crate either;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate unicode_segmentation; extern crate unicode_segmentation;

View File

@ -40,13 +40,6 @@ pub enum Modifier {
Underline, Underline,
} }
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style { pub struct Style {
pub fg: Color, pub fg: Color,

View File

@ -14,7 +14,7 @@ pub use self::block::Block;
pub use self::chart::{Axis, Chart, Dataset, Marker}; pub use self::chart::{Axis, Chart, Dataset, Marker};
pub use self::gauge::Gauge; pub use self::gauge::Gauge;
pub use self::list::{Item, List, SelectableList}; pub use self::list::{Item, List, SelectableList};
pub use self::paragraph::Paragraph; pub use self::paragraph::{Paragraph, Text};
pub use self::sparkline::Sparkline; pub use self::sparkline::Sparkline;
pub use self::table::{Row, Table}; pub use self::table::{Row, Table};
pub use self::tabs::Tabs; pub use self::tabs::Tabs;

View File

@ -1,30 +1,38 @@
use either::Either;
use itertools::{multipeek, MultiPeek}; use itertools::{multipeek, MultiPeek};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use buffer::Buffer; use buffer::Buffer;
use layout::Rect; use layout::{Alignment, Rect};
use style::{Alignment, Color, Modifier, Style}; use style::Style;
use widgets::{Block, Widget}; use widgets::{Block, Widget};
/// A widget to display some text. You can specify colors using commands embedded in /// A widget to display some text.
/// the text such as "{[color] [text]}".
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// # extern crate tui; /// # extern crate tui;
/// # use tui::widgets::{Block, Borders, Paragraph}; /// # use tui::widgets::{Block, Borders, Paragraph, Text};
/// # use tui::style::{Style, Color}; /// # use tui::style::{Style, Color};
/// # use tui::layout::{Alignment};
/// # fn main() { /// # fn main() {
/// Paragraph::default() /// let text = [
/// Text::Data("First line\n"),
/// Text::StyledData("Second line\n", Style::default().fg(Color::Red))
/// ];
/// Paragraph::new(text.iter())
/// .block(Block::default().title("Paragraph").borders(Borders::ALL)) /// .block(Block::default().title("Paragraph").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White).bg(Color::Black)) /// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .wrap(true) /// .alignment(Alignment::Center)
/// .text("First line\nSecond line\n{red Colored text}."); /// .wrap(true);
/// # } /// # }
/// ``` /// ```
pub struct Paragraph<'a> { pub struct Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
/// A block to wrap the widget in /// A block to wrap the widget in
block: Option<Block<'a>>, block: Option<Block<'a>>,
/// Widget style /// Widget style
@ -32,7 +40,7 @@ pub struct Paragraph<'a> {
/// Wrap the text or not /// Wrap the text or not
wrapping: bool, wrapping: bool,
/// The text to display /// The text to display
text: &'a str, text: T,
/// Should we parse the text for embedded commands /// Should we parse the text for embedded commands
raw: bool, raw: bool,
/// Scroll /// Scroll
@ -41,195 +49,62 @@ pub struct Paragraph<'a> {
alignment: Alignment, alignment: Alignment,
} }
impl<'a> Default for Paragraph<'a> { pub enum Text<'b> {
fn default() -> Paragraph<'a> { Data(&'b str),
StyledData(&'b str, Style),
}
impl<'a, 't, T> Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
pub fn new(text: T) -> Paragraph<'a, 't, T> {
Paragraph { Paragraph {
block: None, block: None,
style: Default::default(), style: Default::default(),
wrapping: false, wrapping: false,
raw: false, raw: false,
text: "", text,
scroll: 0, scroll: 0,
alignment: Alignment::Left, alignment: Alignment::Left,
} }
} }
}
impl<'a> Paragraph<'a> { pub fn block(&'a mut self, block: Block<'a>) -> &mut Paragraph<'a, 't, T> {
pub fn block(&'a mut self, block: Block<'a>) -> &mut Paragraph<'a> {
self.block = Some(block); self.block = Some(block);
self self
} }
pub fn text(&mut self, text: &'a str) -> &mut Paragraph<'a> { pub fn style(&mut self, style: Style) -> &mut Paragraph<'a, 't, T> {
self.text = text;
self
}
pub fn style(&mut self, style: Style) -> &mut Paragraph<'a> {
self.style = style; self.style = style;
self self
} }
pub fn wrap(&mut self, flag: bool) -> &mut Paragraph<'a> { pub fn wrap(&mut self, flag: bool) -> &mut Paragraph<'a, 't, T> {
self.wrapping = flag; self.wrapping = flag;
self self
} }
pub fn raw(&mut self, flag: bool) -> &mut Paragraph<'a> { pub fn raw(&mut self, flag: bool) -> &mut Paragraph<'a, 't, T> {
self.raw = flag; self.raw = flag;
self self
} }
pub fn scroll(&mut self, offset: u16) -> &mut Paragraph<'a> { pub fn scroll(&mut self, offset: u16) -> &mut Paragraph<'a, 't, T> {
self.scroll = offset; self.scroll = offset;
self self
} }
pub fn alignment(&mut self, alignment: Alignment) -> &mut Paragraph<'a> { pub fn alignment(&mut self, alignment: Alignment) -> &mut Paragraph<'a, 't, T> {
self.alignment = alignment; self.alignment = alignment;
self self
} }
} }
struct Parser<'a, T> impl<'a, 't, T> Widget for Paragraph<'a, 't, T>
where where
T: Iterator<Item = &'a str>, T: Iterator<Item = &'t Text<'t>>,
{ {
text: T,
mark: bool,
cmd_string: String,
style: Style,
base_style: Style,
escaping: bool,
styling: bool,
}
impl<'a, T> Parser<'a, T>
where
T: Iterator<Item = &'a str>,
{
fn new(text: T, base_style: Style) -> Parser<'a, T> {
Parser {
text: text,
mark: false,
cmd_string: String::from(""),
style: base_style,
base_style: base_style,
escaping: false,
styling: false,
}
}
fn update_style(&mut self) {
for cmd in self.cmd_string.split(';') {
let args = cmd.split('=').collect::<Vec<&str>>();
if let Some(first) = args.get(0) {
match *first {
"fg" => if let Some(snd) = args.get(1) {
self.style.fg = Parser::<T>::str_to_color(snd);
},
"bg" => if let Some(snd) = args.get(1) {
self.style.bg = Parser::<T>::str_to_color(snd);
},
"mod" => if let Some(snd) = args.get(1) {
self.style.modifier = Parser::<T>::str_to_modifier(snd);
},
_ => {}
}
}
}
}
fn str_to_color(string: &str) -> Color {
match string {
"black" => Color::Black,
"red" => Color::Red,
"green" => Color::Green,
"yellow" => Color::Yellow,
"blue" => Color::Blue,
"magenta" => Color::Magenta,
"cyan" => Color::Cyan,
"gray" => Color::Gray,
"dark_gray" => Color::DarkGray,
"light_red" => Color::LightRed,
"light_green" => Color::LightGreen,
"light_blue" => Color::LightBlue,
"light_yellow" => Color::LightYellow,
"light_magenta" => Color::LightMagenta,
"light_cyan" => Color::LightCyan,
"white" => Color::White,
_ => Color::Reset,
}
}
fn str_to_modifier(string: &str) -> Modifier {
match string {
"bold" => Modifier::Bold,
"italic" => Modifier::Italic,
"underline" => Modifier::Underline,
"invert" => Modifier::Invert,
"crossed_out" => Modifier::CrossedOut,
_ => Modifier::Reset,
}
}
fn reset(&mut self) {
self.styling = false;
self.mark = false;
self.style = self.base_style;
self.cmd_string.clear();
}
}
impl<'a, T> Iterator for Parser<'a, T>
where
T: Iterator<Item = &'a str>,
{
type Item = (&'a str, Style);
fn next(&mut self) -> Option<Self::Item> {
match self.text.next() {
Some(s) => if s == "\\" {
if self.escaping {
Some((s, self.style))
} else {
self.escaping = true;
self.next()
}
} else if s == "{" {
if self.escaping {
self.escaping = false;
Some((s, self.style))
} else if self.mark {
Some((s, self.style))
} else {
self.style = self.base_style;
self.mark = true;
self.next()
}
} else if s == "}" && self.mark {
self.reset();
self.next()
} else if s == " " && self.mark {
if self.styling {
Some((s, self.style))
} else {
self.styling = true;
self.update_style();
self.next()
}
} else if self.mark && !self.styling {
self.cmd_string.push_str(s);
self.next()
} else {
Some((s, self.style))
},
None => None,
}
}
}
impl<'a> Widget for Paragraph<'a> {
fn draw(&mut self, area: &Rect, buf: &mut Buffer) { fn draw(&mut self, area: &Rect, buf: &mut Buffer) {
let text_area = match self.block { let text_area = match self.block {
Some(ref mut b) => { Some(ref mut b) => {
@ -245,13 +120,15 @@ impl<'a> Widget for Paragraph<'a> {
self.background(&text_area, buf, self.style.bg); self.background(&text_area, buf, self.style.bg);
let graphemes = UnicodeSegmentation::graphemes(self.text, true); let style = self.style;
let styled: Box<Iterator<Item = (&str, Style)>> = if self.raw { let styled = self.text.by_ref().flat_map(|t| match t {
Box::new(graphemes.map(|g| (g, self.style))) &Text::Data(d) => {
} else { Either::Left(UnicodeSegmentation::graphemes(d, true).map(|g| (g, style)))
Box::new(Parser::new(graphemes, self.style)) }
}; &Text::StyledData(d, s) => {
Either::Right(UnicodeSegmentation::graphemes(d, true).map(move |g| (g, s)))
}
});
let mut styled = multipeek(styled); let mut styled = multipeek(styled);
fn get_cur_line_len<'a, I: Iterator<Item = (&'a str, Style)>>( fn get_cur_line_len<'a, I: Iterator<Item = (&'a str, Style)>>(