diff --git a/Cargo.toml b/Cargo.toml index c56287ad..40b482b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ bitflags = "1.0.1" cassowary = "0.3.0" itertools = "0.7.8" log = "0.4.1" +either = "1.5.0" unicode-segmentation = "1.2.0" unicode-width = "0.1.4" termion = { version = "1.5.1", optional = true } diff --git a/examples/demo.rs b/examples/demo.rs index b942acd1..82891ffa 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -20,7 +20,7 @@ use tui::style::{Color, Modifier, Style}; use tui::widgets::canvas::{Canvas, Line, Map, MapResolution}; use tui::widgets::{ 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}; @@ -194,7 +194,7 @@ fn main() { let tx = tx.clone(); loop { 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, app: &App, area: &Rect) { } fn draw_text(f: &mut Frame, 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::default() .borders(Borders::ALL) @@ -448,17 +465,6 @@ fn draw_text(f: &mut Frame, area: &Rect) { .title_style(Style::default().fg(Color::Magenta).modifier(Modifier::Bold)), ) .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); } diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 0848ca4e..67006739 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -6,9 +6,9 @@ use termion::event; use termion::input::TermRead; use tui::backend::MouseBackend; -use tui::layout::{Constraint, Direction, Layout, Rect}; -use tui::style::{Alignment, Color, Style}; -use tui::widgets::{Block, Paragraph, Widget}; +use tui::layout::{Alignment, Constraint, Direction, Layout, Rect}; +use tui::style::{Color, Style, Modifier}; +use tui::widgets::{Block, Paragraph, Widget, Text}; use tui::Terminal; fn main() { @@ -55,38 +55,24 @@ fn draw(t: &mut Terminal, size: &Rect) { ) .split(size); - Paragraph::default() - .alignment(Alignment::Left) - .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[0]); + let text = [ + Text::Data("This a line\n"), + Text::StyledData("This a line\n", Style::default().fg(Color::Red)), + Text::StyledData("This a line\n", Style::default().bg(Color::Blue)), + Text::StyledData("This a longer line\n", Style::default().modifier(Modifier::CrossedOut)), + Text::StyledData("This a line\n", Style::default().fg(Color::Green).modifier(Modifier::Italic)), + ]; - Paragraph::default() + Paragraph::new(text.iter()) + .alignment(Alignment::Left) + .render(&mut f, &chunks[0]); + Paragraph::new(text.iter()) .alignment(Alignment::Center) .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]); - Paragraph::default() + Paragraph::new(text.iter()) .alignment(Alignment::Right) .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]); } t.draw().unwrap(); diff --git a/examples/user_input.rs b/examples/user_input.rs index 0f741835..cb3ffdf7 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -22,7 +22,7 @@ use termion::input::TermRead; use tui::backend::MouseBackend; use tui::layout::{Constraint, Direction, Layout, Rect}; 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; struct App { @@ -115,10 +115,9 @@ fn draw(t: &mut Terminal, app: &App) { .margin(2) .constraints([Constraint::Length(3), Constraint::Min(1)].as_ref()) .split(&app.size); - Paragraph::default() + Paragraph::new([Text::Data(&app.input)].iter()) .style(Style::default().fg(Color::Yellow)) .block(Block::default().borders(Borders::ALL).title("Input")) - .text(&app.input) .render(&mut f, &chunks[0]); List::new( app.messages diff --git a/src/layout.rs b/src/layout.rs index d86415bd..99fd870d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -29,6 +29,13 @@ pub enum Constraint { Min(u16), } +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Alignment { + Left, + Center, + Right, +} + // TODO: enforce constraints size once const generics has landed #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Layout { diff --git a/src/lib.rs b/src/lib.rs index 1af46fe3..098c20d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -167,6 +167,7 @@ extern crate bitflags; extern crate cassowary; extern crate itertools; +extern crate either; #[macro_use] extern crate log; extern crate unicode_segmentation; diff --git a/src/style.rs b/src/style.rs index 5c267a78..f58e1551 100644 --- a/src/style.rs +++ b/src/style.rs @@ -40,13 +40,6 @@ pub enum Modifier { Underline, } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Alignment { - Left, - Center, - Right, -} - #[derive(Debug, Clone, Copy, PartialEq)] pub struct Style { pub fg: Color, diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 4fd42025..18097ef2 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -14,7 +14,7 @@ pub use self::block::Block; pub use self::chart::{Axis, Chart, Dataset, Marker}; pub use self::gauge::Gauge; 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::table::{Row, Table}; pub use self::tabs::Tabs; diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index cbe52e80..2bb4959e 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -1,30 +1,38 @@ +use either::Either; use itertools::{multipeek, MultiPeek}; use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use buffer::Buffer; -use layout::Rect; -use style::{Alignment, Color, Modifier, Style}; +use layout::{Alignment, Rect}; +use style::Style; use widgets::{Block, Widget}; -/// A widget to display some text. You can specify colors using commands embedded in -/// the text such as "{[color] [text]}". +/// A widget to display some text. /// /// # Examples /// /// ``` /// # extern crate tui; -/// # use tui::widgets::{Block, Borders, Paragraph}; +/// # use tui::widgets::{Block, Borders, Paragraph, Text}; /// # use tui::style::{Style, Color}; +/// # use tui::layout::{Alignment}; /// # 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)) /// .style(Style::default().fg(Color::White).bg(Color::Black)) -/// .wrap(true) -/// .text("First line\nSecond line\n{red Colored text}."); +/// .alignment(Alignment::Center) +/// .wrap(true); /// # } /// ``` -pub struct Paragraph<'a> { +pub struct Paragraph<'a, 't, T> +where + T: Iterator>, +{ /// A block to wrap the widget in block: Option>, /// Widget style @@ -32,7 +40,7 @@ pub struct Paragraph<'a> { /// Wrap the text or not wrapping: bool, /// The text to display - text: &'a str, + text: T, /// Should we parse the text for embedded commands raw: bool, /// Scroll @@ -41,195 +49,62 @@ pub struct Paragraph<'a> { alignment: Alignment, } -impl<'a> Default for Paragraph<'a> { - fn default() -> Paragraph<'a> { +pub enum Text<'b> { + Data(&'b str), + StyledData(&'b str, Style), +} + +impl<'a, 't, T> Paragraph<'a, 't, T> +where + T: Iterator>, +{ + pub fn new(text: T) -> Paragraph<'a, 't, T> { Paragraph { block: None, style: Default::default(), wrapping: false, raw: false, - text: "", + text, scroll: 0, alignment: Alignment::Left, } } -} -impl<'a> Paragraph<'a> { - pub fn block(&'a mut self, block: Block<'a>) -> &mut Paragraph<'a> { + pub fn block(&'a mut self, block: Block<'a>) -> &mut Paragraph<'a, 't, T> { self.block = Some(block); self } - pub fn text(&mut self, text: &'a str) -> &mut Paragraph<'a> { - self.text = text; - self - } - - pub fn style(&mut self, style: Style) -> &mut Paragraph<'a> { + pub fn style(&mut self, style: Style) -> &mut Paragraph<'a, 't, T> { self.style = style; 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 } - 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 } - 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 } - 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 } } -struct Parser<'a, T> +impl<'a, 't, T> Widget for Paragraph<'a, 't, T> where - T: Iterator, + T: Iterator>, { - 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, -{ - 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::>(); - if let Some(first) = args.get(0) { - match *first { - "fg" => if let Some(snd) = args.get(1) { - self.style.fg = Parser::::str_to_color(snd); - }, - "bg" => if let Some(snd) = args.get(1) { - self.style.bg = Parser::::str_to_color(snd); - }, - "mod" => if let Some(snd) = args.get(1) { - self.style.modifier = Parser::::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, -{ - type Item = (&'a str, Style); - fn next(&mut self) -> Option { - 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) { let text_area = match self.block { Some(ref mut b) => { @@ -245,13 +120,15 @@ impl<'a> Widget for Paragraph<'a> { self.background(&text_area, buf, self.style.bg); - let graphemes = UnicodeSegmentation::graphemes(self.text, true); - let styled: Box> = if self.raw { - Box::new(graphemes.map(|g| (g, self.style))) - } else { - Box::new(Parser::new(graphemes, self.style)) - }; - + let style = self.style; + let styled = self.text.by_ref().flat_map(|t| match t { + &Text::Data(d) => { + Either::Left(UnicodeSegmentation::graphemes(d, true).map(|g| (g, style))) + } + &Text::StyledData(d, s) => { + Either::Right(UnicodeSegmentation::graphemes(d, true).map(move |g| (g, s))) + } + }); let mut styled = multipeek(styled); fn get_cur_line_len<'a, I: Iterator>(