diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index 928d96d4..1b32b080 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -5,8 +5,8 @@ use tui::layout::{Constraint, Direction, Layout, Rect}; use tui::style::{Color, Modifier, Style}; use tui::widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle}; 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}; @@ -103,28 +103,35 @@ where .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .direction(Direction::Horizontal) .split(chunks[0]); - SelectableList::default() + let items = app.tasks.items.iter().map(|i| Text::raw(*i)).collect(); + List::new(items) .block(Block::default().borders(Borders::ALL).title("List")) - .items(&app.tasks.items) .select(Some(app.tasks.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.logs.items.iter().map(|&(evt, level)| { - Text::styled( - format!("{}: {}", level, evt), - match level { + let events = app + .logs + .items + .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 05dc4cbd..44885f84 100644 --- a/examples/layout.rs +++ b/examples/layout.rs @@ -15,6 +15,10 @@ use tui::Terminal; use crate::util::event::{Event, Events}; fn main() -> Result<(), failure::Error> { + stderrlog::new() + .module(module_path!()) + .verbosity(4) + .init()?; // Terminal initialization let stdout = io::stdout().into_raw_mode()?; let stdout = MouseTerminal::from(stdout); @@ -40,7 +44,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 8a3ee193..b36ee25a 100644 --- a/examples/list.rs +++ b/examples/list.rs @@ -10,7 +10,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 crate::util::event::{Event, Events}; @@ -29,13 +29,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"), @@ -62,7 +83,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), @@ -97,26 +118,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 83f1455f..ac85f96d 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -22,7 +22,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; @@ -76,7 +76,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 f50cc1ef..6adecbf0 100644 --- a/src/backend/rustbox.rs +++ b/src/backend/rustbox.rs @@ -12,7 +12,7 @@ pub struct RustboxBackend { impl RustboxBackend { pub fn new() -> Result { - let rustbox = r#try!(rustbox::RustBox::init(Default::default())); + let rustbox = rustbox::RustBox::init(Default::default())?; Ok(RustboxBackend { rustbox }) } diff --git a/src/backend/termion.rs b/src/backend/termion.rs index 134eacf1..0aed8200 100644 --- a/src/backend/termion.rs +++ b/src/backend/termion.rs @@ -128,7 +128,7 @@ where /// Return the size of the terminal fn size(&self) -> io::Result { - let terminal = r#try!(termion::terminal_size()); + let terminal = termion::terminal_size()?; Ok(Rect::new(0, 0, terminal.0, terminal.1)) } diff --git a/src/widgets/list.rs b/src/widgets/list.rs index cf3992ba..4b0d772b 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -1,6 +1,3 @@ -use std::convert::AsRef; -use std::iter::{self, Iterator}; - use unicode_width::UnicodeWidthStr; use crate::buffer::Buffer; @@ -8,238 +5,167 @@ use crate::layout::{Corner, Rect}; use crate::style::Style; use crate::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 -/// -/// ``` -/// # 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(AsRef::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 cf726cf7..761f2fe2 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,5 +1,7 @@ use bitflags::bitflags; +use either::Either; use std::borrow::Cow; +use unicode_segmentation::UnicodeSegmentation; mod barchart; mod block; @@ -17,7 +19,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}; @@ -49,16 +51,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 151508c6..69c4e1ed 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -1,5 +1,3 @@ -use either::Either; -use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use crate::buffer::Buffer; @@ -123,17 +121,11 @@ where self.background(text_area, buf, self.style.bg); let style = self.style; - let mut 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| Styled(g, style))) - } - Text::Styled(ref d, s) => { - let data: &'t str = d; // coerce to &str - Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s))) - } - }); - + let mut styled = self + .text + .by_ref() + .flat_map(|t| t.styled_graphemes(style)) + .map(|(symbol, style)| Styled(symbol, style)); let mut line_composer: Box = if self.wrapping { Box::new(WordWrapper::new(&mut styled, text_area.width)) } else { diff --git a/src/widgets/reflow.rs b/src/widgets/reflow.rs index 32e951a3..670bc651 100644 --- a/src/widgets/reflow.rs +++ b/src/widgets/reflow.rs @@ -1,6 +1,7 @@ -use crate::style::Style; use unicode_width::UnicodeWidthStr; +use crate::style::Style; + const NBSP: &str = "\u{00a0}"; #[derive(Copy, Clone, Debug)] diff --git a/tests/block.rs b/tests/block.rs index e289a0e4..138a1672 100644 --- a/tests/block.rs +++ b/tests/block.rs @@ -26,18 +26,20 @@ fn it_draws_a_block() { ); }) .unwrap(); - let mut expected = Buffer::with_lines(vec![ - "┌Title─┐ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "│ │ ", - "└──────┘ ", - " ", - " ", - ]); + let mut expected = Buffer::with_lines( + vec![ + "┌Title─┐ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "│ │ ", + "└──────┘ ", + " ", + " ", + ] + ); for x in 1..=5 { expected.get_mut(x, 0).set_fg(Color::LightBlue); }