diff --git a/examples/demo.rs b/examples/demo.rs index f7f78037..ac881cda 100644 --- a/examples/demo.rs +++ b/examples/demo.rs @@ -18,8 +18,8 @@ use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::style::{Color, Modifier, Style}; use tui::widgets::canvas::{Canvas, Line, Map, MapResolution}; use tui::widgets::{ - Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row, - SelectableList, Sparkline, Table, Tabs, Text, Widget, + Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Marker, Paragraph, Row, Sparkline, + Table, Tabs, Text, Widget, }; use tui::{Frame, Terminal}; @@ -315,28 +315,34 @@ where .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .direction(Direction::Horizontal) .split(chunks[0]); - SelectableList::default() + let items = app.items.iter().map(|i| Text::raw(*i)).collect(); + List::new(items) .block(Block::default().borders(Borders::ALL).title("List")) - .items(&app.items) .select(Some(app.selected)) .highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::Bold)) .highlight_symbol(">") .render(f, chunks[0]); - let info_style = Style::default().fg(Color::White); + let info_style = Style::default().fg(Color::Blue); let warning_style = Style::default().fg(Color::Yellow); let error_style = Style::default().fg(Color::Magenta); let critical_style = Style::default().fg(Color::Red); - let events = app.events.iter().map(|&(evt, level)| { - Text::styled( - format!("{}: {}", level, evt), - match level { + let events = app + .events + .iter() + .map(|&(evt, level)| { + let level_style = match level { "ERROR" => error_style, "CRITICAL" => critical_style, "WARNING" => warning_style, - _ => info_style, - }, - ) - }); + "INFO" => info_style, + _ => Style::default(), + }; + Text::with_styles(vec![ + (format!("{:<10}", level), level_style), + (format!(" : {}", evt), Style::default()), + ]) + }) + .collect(); List::new(events) .block(Block::default().borders(Borders::ALL).title("List")) .render(f, chunks[1]); diff --git a/examples/layout.rs b/examples/layout.rs index c8c1ea5f..c4c7781e 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -21,7 +21,10 @@ use tui::Terminal; use util::event::{Event, Events}; fn main() -> Result<(), failure::Error> { - stderrlog::new().verbosity(4).init()?; + stderrlog::new() + .module(module_path!()) + .verbosity(4) + .init()?; // Terminal initialization let stdout = io::stdout().into_raw_mode()?; @@ -48,7 +51,7 @@ fn main() -> Result<(), failure::Error> { .split(f.size()); Block::default() - .title("Block") + .title("Block 1") .borders(Borders::ALL) .render(&mut f, chunks[0]); Block::default() diff --git a/examples/list.rs b/examples/list.rs index b504139a..d819cdc9 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -14,7 +14,7 @@ use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Corner, Direction, Layout}; use tui::style::{Color, Modifier, Style}; -use tui::widgets::{Block, Borders, List, SelectableList, Text, Widget}; +use tui::widgets::{Block, Borders, List, Text, Widget}; use tui::Terminal; use util::event::{Event, Events}; @@ -33,13 +33,34 @@ impl<'a> App<'a> { fn new() -> App<'a> { App { items: vec![ - "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", "Item9", - "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", "Item17", - "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24", + "Item1\nItem11\nItem12", + "Item2222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + "Item3", + "Item4", + "Item5", + "Item6", + "Item7", + "Item8", + "Item9", + "Item10", + "Item11", + "Item12", + "Item13", + "Item14", + "Item15", + "Item16", + "Item17", + "Item18", + "Item19", + "Item20", + "Item21", + "Item22", + "Item23", + "Item24", ], selected: None, events: vec![ - ("Event1", "INFO"), + ("Event1\nlorem ipsum", "INFO"), ("Event2", "INFO"), ("Event3", "CRITICAL"), ("Event4", "ERROR"), @@ -66,7 +87,7 @@ impl<'a> App<'a> { ("Event25", "INFO"), ("Event26", "INFO"), ], - info_style: Style::default().fg(Color::White), + info_style: Style::default().fg(Color::Blue), warning_style: Style::default().fg(Color::Yellow), error_style: Style::default().fg(Color::Magenta), critical_style: Style::default().fg(Color::Red), @@ -101,26 +122,39 @@ fn main() -> Result<(), failure::Error> { .split(f.size()); let style = Style::default().fg(Color::Black).bg(Color::White); - SelectableList::default() - .block(Block::default().borders(Borders::ALL).title("List")) - .items(&app.items) - .select(app.selected) - .style(style) - .highlight_style(style.fg(Color::LightGreen).modifier(Modifier::Bold)) - .highlight_symbol(">") - .render(&mut f, chunks[0]); { - let events = app.events.iter().map(|&(evt, level)| { - Text::styled( - format!("{}: {}", level, evt), - match level { + let items = app + .items + .iter() + .map(|i| Text::raw(*i)) + .collect::>(); + List::new(items) + .block(Block::default().borders(Borders::ALL).title("List")) + .style(style) + .select(app.selected) + .highlight_style(style.fg(Color::LightGreen).modifier(Modifier::Bold)) + .highlight_symbol(">") + .render(&mut f, chunks[0]); + } + { + let events = app + .events + .iter() + .map(|&(evt, level)| { + let level_style = match level { "ERROR" => app.error_style, "CRITICAL" => app.critical_style, "WARNING" => app.warning_style, - _ => app.info_style, - }, - ) - }); + "INFO" => app.info_style, + _ => Default::default(), + }; + Text::with_styles(vec![ + (format!("{:<10}", level), level_style), + (" : ".to_owned(), Default::default()), + (evt.to_owned(), Default::default()), + ]) + }) + .collect(); List::new(events) .block(Block::default().borders(Borders::ALL).title("List")) .start_corner(Corner::BottomLeft) diff --git a/examples/user_input.rs b/examples/user_input.rs index 3a7ed44e..71cd41b8 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -26,7 +26,7 @@ use termion::raw::IntoRawMode; use termion::screen::AlternateScreen; use tui::backend::TermionBackend; use tui::layout::{Constraint, Direction, Layout}; -use tui::style::{Color, Style}; +use tui::style::{Color, Modifier, Style}; use tui::widgets::{Block, Borders, List, Paragraph, Text, Widget}; use tui::Terminal; use unicode_width::UnicodeWidthStr; @@ -80,7 +80,13 @@ fn main() -> Result<(), failure::Error> { .messages .iter() .enumerate() - .map(|(i, m)| Text::raw(format!("{}: {}", i, m))); + .map(|(i, m)| { + Text::with_styles(vec![ + (format!("{}", i), Style::default().modifier(Modifier::Bold)), + (format!(" : {}", m), Style::default()), + ]) + }) + .collect(); List::new(messages) .block(Block::default().borders(Borders::ALL).title("Messages")) .render(&mut f, chunks[1]); diff --git a/src/backend/rustbox.rs b/src/backend/rustbox.rs index 375190b7..4360028d 100644 --- a/src/backend/rustbox.rs +++ b/src/backend/rustbox.rs @@ -13,8 +13,8 @@ pub struct RustboxBackend { impl RustboxBackend { pub fn new() -> Result { - let rustbox = try!(rustbox::RustBox::init(Default::default())); - Ok(RustboxBackend { rustbox: rustbox }) + let rustbox = rustbox::RustBox::init(Default::default())?; + Ok(RustboxBackend { rustbox }) } pub fn with_rustbox(instance: rustbox::RustBox) -> RustboxBackend { diff --git a/src/backend/termion.rs b/src/backend/termion.rs index 8590c775..c9a982d1 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -111,7 +111,7 @@ where /// Return the size of the terminal fn size(&self) -> io::Result { - let terminal = try!(termion::terminal_size()); + let terminal = termion::terminal_size()?; Ok(Rect::new(0, 0, terminal.0, terminal.1)) } diff --git a/src/buffer.rs b/src/buffer.rs index 49ee6ba0..42d08e78 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -157,7 +157,7 @@ impl Buffer { } /// Returns a Buffer containing the given lines - pub fn with_lines(lines: Vec) -> Buffer + pub fn with_lines(lines: &[S]) -> Buffer where S: AsRef, { @@ -171,10 +171,8 @@ impl Buffer { width, height, }); - let mut y = 0; - for line in &lines { - buffer.set_string(0, y, line, Style::default()); - y += 1; + for (y, line) in lines.iter().enumerate() { + buffer.set_string(0, y as u16, line, Style::default()); } buffer } diff --git a/src/widgets/list.rs b/src/widgets/list.rs index d9c3955d..d1f8b272 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -1,6 +1,3 @@ -use std::iter; -use std::iter::Iterator; - use unicode_width::UnicodeWidthStr; use buffer::Buffer; @@ -8,239 +5,164 @@ use layout::{Corner, Rect}; use style::Style; use widgets::{Block, Text, Widget}; -pub struct List<'b, L> -where - L: Iterator>, -{ - block: Option>, - items: L, +pub struct List<'a> { + block: Option>, + items: Vec>, style: Style, start_corner: Corner, -} - -impl<'b, L> Default for List<'b, L> -where - L: Iterator> + Default, -{ - fn default() -> List<'b, L> { - List { - block: None, - items: L::default(), - style: Default::default(), - start_corner: Corner::TopLeft, - } - } -} - -impl<'b, L> List<'b, L> -where - L: Iterator>, -{ - pub fn new(items: L) -> List<'b, L> { - List { - block: None, - items, - style: Default::default(), - start_corner: Corner::TopLeft, - } - } - - pub fn block(mut self, block: Block<'b>) -> List<'b, L> { - self.block = Some(block); - self - } - - pub fn items(mut self, items: I) -> List<'b, L> - where - I: IntoIterator, IntoIter = L>, - { - self.items = items.into_iter(); - self - } - - pub fn style(mut self, style: Style) -> List<'b, L> { - self.style = style; - self - } - - pub fn start_corner(mut self, corner: Corner) -> List<'b, L> { - self.start_corner = corner; - self - } -} - -impl<'b, L> Widget for List<'b, L> -where - L: Iterator>, -{ - fn draw(&mut self, area: Rect, buf: &mut Buffer) { - let list_area = match self.block { - Some(ref mut b) => { - b.draw(area, buf); - b.inner(area) - } - None => area, - }; - - if list_area.width < 1 || list_area.height < 1 { - return; - } - - self.background(&list_area, buf, self.style.bg); - - for (i, item) in self - .items - .by_ref() - .enumerate() - .take(list_area.height as usize) - { - let (x, y) = match self.start_corner { - Corner::TopLeft => (list_area.left(), list_area.top() + i as u16), - Corner::BottomLeft => (list_area.left(), list_area.bottom() - (i + 1) as u16), - // Not supported - _ => (list_area.left(), list_area.top() + i as u16), - }; - match item { - Text::Raw(ref v) => { - buf.set_stringn(x, y, v, list_area.width as usize, Style::default()); - } - Text::Styled(ref v, s) => { - buf.set_stringn(x, y, v, list_area.width as usize, s); - } - }; - } - } -} - -/// A widget to display several items among which one can be selected (optional) -/// -/// # Examples -/// -/// ``` -/// # extern crate tui; -/// # use tui::widgets::{Block, Borders, SelectableList}; -/// # use tui::style::{Style, Color, Modifier}; -/// # fn main() { -/// SelectableList::default() -/// .block(Block::default().title("SelectableList").borders(Borders::ALL)) -/// .items(&["Item 1", "Item 2", "Item 3"]) -/// .select(Some(1)) -/// .style(Style::default().fg(Color::White)) -/// .highlight_style(Style::default().modifier(Modifier::Italic)) -/// .highlight_symbol(">>"); -/// # } -/// ``` -pub struct SelectableList<'b> { - block: Option>, - /// Items to be displayed - items: Vec<&'b str>, /// Index of the one selected selected: Option, - /// Base style of the widget - style: Style, /// Style used to render selected item highlight_style: Style, /// Symbol in front of the selected item (Shift all items to the right) - highlight_symbol: Option<&'b str>, + highlight_symbol: Option<&'a str>, } -impl<'b> Default for SelectableList<'b> { - fn default() -> SelectableList<'b> { - SelectableList { +impl<'a> List<'a> { + pub fn new(items: Vec>) -> List<'a> { + List { block: None, - items: Vec::new(), - selected: None, + items: items.into(), style: Default::default(), + start_corner: Corner::TopLeft, + selected: None, highlight_style: Default::default(), highlight_symbol: None, } } -} -impl<'b> SelectableList<'b> { - pub fn block(mut self, block: Block<'b>) -> SelectableList<'b> { + pub fn block(mut self, block: Block<'a>) -> List<'a> { self.block = Some(block); self } - pub fn items(mut self, items: &'b [I]) -> SelectableList<'b> - where - I: AsRef + 'b, - { - self.items = items.iter().map(|i| i.as_ref()).collect::>(); + pub fn style(mut self, style: Style) -> List<'a> { + self.style = style; self } - pub fn style(mut self, style: Style) -> SelectableList<'b> { - self.style = style; + pub fn start_corner(mut self, corner: Corner) -> List<'a> { + self.start_corner = corner; self } - pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> SelectableList<'b> { - self.highlight_symbol = Some(highlight_symbol); + pub fn select(mut self, index: Option) -> List<'a> { + self.selected = index; self } - pub fn highlight_style(mut self, highlight_style: Style) -> SelectableList<'b> { - self.highlight_style = highlight_style; + pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> { + self.highlight_symbol = Some(highlight_symbol); self } - pub fn select(mut self, index: Option) -> SelectableList<'b> { - self.selected = index; + pub fn highlight_style(mut self, highlight_style: Style) -> List<'a> { + self.highlight_style = highlight_style; self } } -impl<'b> Widget for SelectableList<'b> { +impl<'a> Widget for List<'a> { fn draw(&mut self, area: Rect, buf: &mut Buffer) { let list_area = match self.block { - Some(ref mut b) => b.inner(area), + Some(ref mut b) => { + b.draw(area, buf); + b.inner(area) + } None => area, }; - let list_height = list_area.height as usize; + if list_area.width < 1 || list_area.height < 1 { + return; + } + + self.background(&list_area, buf, self.style.bg); // Use highlight_style only if something is selected let (selected, highlight_style) = match self.selected { Some(i) => (Some(i), self.highlight_style), None => (None, self.style), }; - let highlight_symbol = self.highlight_symbol.unwrap_or(""); - let blank_symbol = iter::repeat(" ") - .take(highlight_symbol.width()) - .collect::(); + let (x_offset, highlight_symbol) = if selected.is_some() { + if let Some(symbol) = self.highlight_symbol { + (symbol.width() as u16, symbol) + } else { + (0, "") + } + } else { + (0, "") + }; + // Make sure the list show the selected item let offset = if let Some(selected) = selected { - if selected >= list_height { - selected - list_height + 1 - } else { - 0 + // In order to show the selected item, the content will be shifted + // from a certain height. This height is the total height of all + // items that do not fit in the current list height, including + // the selected items + let mut height = 0; + let mut offset_height = 0; + for (i, item) in self.items.iter().enumerate() { + let item_height = item.height(); + if height + item_height > list_area.height { + offset_height = item_height + height - list_area.height; + } + height += item_height; + if i == selected { + break; + } } + + // Go through as much items as needed until their total height is + // greater than the previous found offset_height + let mut offset = 0; + height = 0; + for item in &self.items { + if height >= offset_height { + break; + } + let item_height = item.height(); + height += item_height; + offset += 1; + } + offset } else { 0 }; - // Render items - let items = self - .items - .iter() - .enumerate() - .map(|(i, &item)| { - if let Some(s) = selected { - if i == s { - Text::styled(format!("{} {}", highlight_symbol, item), highlight_style) - } else { - Text::styled(format!("{} {}", blank_symbol, item), self.style) - } - } else { - Text::styled(item, self.style) + let mut height = 0; + for (i, item) in self.items.iter().enumerate().skip(offset) { + let item_height = item.height(); + if height + item_height > list_area.height { + break; + } + let (x, y) = match self.start_corner { + Corner::TopLeft => (list_area.left(), list_area.top() + height), + Corner::BottomLeft => (list_area.left(), list_area.bottom() - (height + item_height)), + // Not supported + _ => (list_area.left(), list_area.top() + height as u16), + }; + height += item_height as u16; + let mut style = self.style; + if let Some(selected) = self.selected { + if i == selected { + buf.set_stringn(x, y, highlight_symbol, x_offset as usize, highlight_style); + style = highlight_style; + } + } + let mut lx = x + x_offset; + let mut ly = y; + for (g, style) in item.styled_graphemes(style) { + if g == "\n" { + ly += 1; + lx = x + x_offset; + continue; } - }) - .skip(offset as usize); - List::new(items) - .block(self.block.unwrap_or_default()) - .style(self.style) - .draw(area, buf); + if lx >= list_area.right() { + continue + } + buf.get_mut(lx, ly).set_symbol(g).set_style(style); + lx += g.width() as u16; + } + } } } diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 63358ac9..eca3d120 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,4 +1,6 @@ +use either::Either; use std::borrow::Cow; +use unicode_segmentation::UnicodeSegmentation; mod barchart; mod block; @@ -15,7 +17,7 @@ pub use self::barchart::BarChart; pub use self::block::Block; pub use self::chart::{Axis, Chart, Dataset, Marker}; pub use self::gauge::Gauge; -pub use self::list::{List, SelectableList}; +pub use self::list::List; pub use self::paragraph::Paragraph; pub use self::sparkline::Sparkline; pub use self::table::{Row, Table}; @@ -47,16 +49,57 @@ bitflags! { pub enum Text<'b> { Raw(Cow<'b, str>), - Styled(Cow<'b, str>, Style), + Styled(Vec<(Cow<'b, str>, Style)>), } impl<'b> Text<'b> { - pub fn raw>>(data: D) -> Text<'b> { + pub fn raw(data: D) -> Text<'b> + where + D: Into>, + { Text::Raw(data.into()) } - pub fn styled>>(data: D, style: Style) -> Text<'b> { - Text::Styled(data.into(), style) + pub fn styled(data: D, style: Style) -> Text<'b> + where + D: Into>, + { + Text::Styled(vec![(data.into(), style)]) + } + + pub fn with_styles(items: Vec<(D, Style)>) -> Text<'b> + where + D: Into>, + { + Text::Styled(items.into_iter().map(|i| (i.0.into(), i.1)).collect()) + } + + pub fn height(&self) -> u16 { + match self { + Text::Raw(ref d) => d.lines().count() as u16, + Text::Styled(items) => { + items + .iter() + .flat_map(|i| i.0.chars()) + .filter(|i| i == &'\n') + .count() as u16 + + 1 + } + } + } + + pub fn styled_graphemes( + &self, + default_style: Style, + ) -> Either, impl Iterator> { + match self { + Text::Raw(d) => Either::Left( + UnicodeSegmentation::graphemes(&**d, true).map(move |g| (g, default_style)), + ), + Text::Styled(items) => Either::Right(items.iter().flat_map(|item| { + UnicodeSegmentation::graphemes(&*item.0, true).map(move |g| (g, item.1)) + })), + } } } diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 29877c66..fbb010a2 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -1,6 +1,4 @@ -use either::Either; use itertools::{multipeek, MultiPeek}; -use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use buffer::Buffer; @@ -116,16 +114,7 @@ where self.background(&text_area, buf, self.style.bg); let style = self.style; - let styled = self.text.by_ref().flat_map(|t| match *t { - Text::Raw(ref d) => { - let data: &'t str = d; // coerce to &str - Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| (g, style))) - } - Text::Styled(ref d, s) => { - let data: &'t str = d; // coerce to &str - Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| (g, s))) - } - }); + let styled = self.text.by_ref().flat_map(|t| t.styled_graphemes(style)); let mut styled = multipeek(styled); fn get_cur_line_len<'a, I: Iterator>( diff --git a/tests/block.rs b/tests/block.rs index a1f7cdec..e2b1fdef 100644 --- a/tests/block.rs +++ b/tests/block.rs @@ -29,18 +29,21 @@ fn it_draws_a_block() { ); }) .unwrap(); - let mut expected = Buffer::with_lines(vec![ - "┌Title─┐ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "└──────┘ ", - " ", - " ", - ]); + let mut expected = Buffer::with_lines( + vec![ + "┌Title─┐ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "└──────┘ ", + " ", + " ", + ] + .as_ref(), + ); for x in 1..=5 { expected.get_mut(x, 0).set_fg(Color::LightBlue); } diff --git a/tests/gauge.rs b/tests/gauge.rs index d4a92033..e1da1c18 100644 --- a/tests/gauge.rs +++ b/tests/gauge.rs @@ -40,6 +40,6 @@ fn gauge_render() { " └──────────────────────────────────┘ ", " ", " ", - ]); + ].as_ref()); assert_eq!(&expected, terminal.backend().buffer()); }