feat(text): add new text primitives

pull/334/head
Florian Dehau 4 years ago
parent 112d2a65f6
commit 88c4b191fb

@ -22,8 +22,6 @@ curses = ["easycurses", "pancurses"]
[dependencies] [dependencies]
bitflags = "1.0" bitflags = "1.0"
cassowary = "0.3" cassowary = "0.3"
itertools = "0.9"
either = "1.5"
unicode-segmentation = "1.2" unicode-segmentation = "1.2"
unicode-width = "0.1" unicode-width = "0.1"
termion = { version = "1.5", optional = true } termion = { version = "1.5", optional = true }

@ -6,6 +6,7 @@ SHELL=/bin/bash
RUST_CHANNEL ?= stable RUST_CHANNEL ?= stable
CARGO_FLAGS = CARGO_FLAGS =
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null) RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
TEST_FILTER ?=
ifndef RUSTUP_INSTALLED ifndef RUSTUP_INSTALLED
CARGO = cargo CARGO = cargo
@ -59,16 +60,16 @@ clippy: ## Check the style of the source code and catch common errors
.PHONY: test .PHONY: test
test: ## Run the tests test: ## Run the tests
$(CARGO) test --all-features $(CARGO) test --all-features $(TEST_FILTER)
# =============================== Examples ==================================== # =============================== Examples ====================================
.PHONY: build-examples .PHONY: build-examples
build-examples: ## Build all examples build-examples: ## Build all examples
@$(CARGO) build --examples --all-features @$(CARGO) build --release --examples --all-features
.PHONY: run-examples .PHONY: run-examples
run-examples: ## Run all examples run-examples: build-examples ## Run all examples
@for file in examples/*.rs; do \ @for file in examples/*.rs; do \
name=$$(basename $${file/.rs/}); \ name=$$(basename $${file/.rs/}); \
$(CARGO) run --all-features --release --example $$name; \ $(CARGO) run --all-features --release --example $$name; \
@ -78,6 +79,7 @@ run-examples: ## Run all examples
.PHONY: doc .PHONY: doc
doc: RUST_CHANNEL = nightly
doc: ## Build the documentation (available at ./target/doc) doc: ## Build the documentation (available at ./target/doc)
$(CARGO) doc $(CARGO) doc
@ -95,8 +97,9 @@ watch-test: ## Watch files changes and run the tests if any
watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test watchman-make -p 'src/**/*.rs' 'tests/**/*.rs' 'examples/**/*.rs' -t test
.PHONY: watch-doc .PHONY: watch-doc
watch-doc: RUST_CHANNEL = nightly
watch-doc: ## Watch file changes and rebuild the documentation if any watch-doc: ## Watch file changes and rebuild the documentation if any
watchman-make -p 'src/**/*.rs' -t doc $(CARGO) watch -x doc -x 'test --doc'
# ================================= Pipelines ================================= # ================================= Pipelines =================================

@ -7,7 +7,8 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style, StyleDiff},
text::Span,
widgets::{Block, BorderType, Borders}, widgets::{Block, BorderType, Borders},
Terminal, Terminal,
}; };
@ -40,39 +41,40 @@ fn main() -> Result<(), Box<dyn Error>> {
.margin(4) .margin(4)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(f.size()); .split(f.size());
{
let chunks = Layout::default() let top_chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[0]); .split(chunks[0]);
let block = Block::default() let block = Block::default()
.title("With background") .title(vec![
.title_style(Style::default().fg(Color::Yellow)) Span::styled("With", StyleDiff::default().fg(Color::Yellow)),
.style(Style::default().bg(Color::Green)); Span::from(" background"),
f.render_widget(block, chunks[0]); ])
let title_style = Style::default() .style(Style::default().bg(Color::Green));
f.render_widget(block, top_chunks[0]);
let block = Block::default().title(Span::styled(
"Styled title",
StyleDiff::default()
.fg(Color::White) .fg(Color::White)
.bg(Color::Red) .bg(Color::Red)
.modifier(Modifier::BOLD); .add_modifier(Modifier::BOLD),
let block = Block::default() ));
.title("Styled title") f.render_widget(block, top_chunks[1]);
.title_style(title_style);
f.render_widget(block, chunks[1]); let bottom_chunks = Layout::default()
} .direction(Direction::Horizontal)
{ .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
let chunks = Layout::default() .split(chunks[1]);
.direction(Direction::Horizontal) let block = Block::default().title("With borders").borders(Borders::ALL);
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) f.render_widget(block, bottom_chunks[0]);
.split(chunks[1]); let block = Block::default()
let block = Block::default().title("With borders").borders(Borders::ALL); .title("With styled borders and doubled borders")
f.render_widget(block, chunks[0]); .border_style(Style::default().fg(Color::Cyan))
let block = Block::default() .borders(Borders::LEFT | Borders::RIGHT)
.title("With styled borders and doubled borders") .border_type(BorderType::Double);
.border_style(Style::default().fg(Color::Cyan)) f.render_widget(block, bottom_chunks[1]);
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double);
f.render_widget(block, chunks[1]);
}
})?; })?;
if let Event::Input(key) = events.next()? { if let Event::Input(key) = events.next()? {

@ -10,8 +10,9 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style, StyleDiff},
symbols, symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType}, widgets::{Axis, Block, Borders, Chart, Dataset, GraphType},
Terminal, Terminal,
}; };
@ -92,12 +93,18 @@ fn main() -> Result<(), Box<dyn Error>> {
.as_ref(), .as_ref(),
) )
.split(size); .split(size);
let x_labels = [ let x_labels = vec![
format!("{}", app.window[0]), Span::styled(
format!("{}", (app.window[0] + app.window[1]) / 2.0), format!("{}", app.window[0]),
format!("{}", app.window[1]), StyleDiff::default().modifier(Modifier::BOLD),
),
Span::raw(format!("{}", (app.window[0] + app.window[1]) / 2.0)),
Span::styled(
format!("{}", app.window[1]),
StyleDiff::default().modifier(Modifier::BOLD),
),
]; ];
let datasets = [ let datasets = vec![
Dataset::default() Dataset::default()
.name("data2") .name("data2")
.marker(symbols::Marker::Dot) .marker(symbols::Marker::Dot)
@ -109,94 +116,118 @@ fn main() -> Result<(), Box<dyn Error>> {
.style(Style::default().fg(Color::Yellow)) .style(Style::default().fg(Color::Yellow))
.data(&app.data2), .data(&app.data2),
]; ];
let chart = Chart::default()
let chart = Chart::new(datasets)
.block( .block(
Block::default() Block::default()
.title("Chart 1") .title(Span::styled(
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD)) "Chart 1",
StyleDiff::default()
.fg(Color::Cyan)
.modifier(Modifier::BOLD),
))
.borders(Borders::ALL), .borders(Borders::ALL),
) )
.x_axis( .x_axis(
Axis::default() Axis::default()
.title("X Axis") .title("X Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC)) .labels(x_labels)
.bounds(app.window) .bounds(app.window),
.labels(&x_labels),
) )
.y_axis( .y_axis(
Axis::default() Axis::default()
.title("Y Axis") .title("Y Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC)) .labels(vec![
.bounds([-20.0, 20.0]) Span::styled("-20", StyleDiff::default().modifier(Modifier::BOLD)),
.labels(&["-20", "0", "20"]), Span::raw("0"),
) Span::styled("20", StyleDiff::default().modifier(Modifier::BOLD)),
.datasets(&datasets); ])
.bounds([-20.0, 20.0]),
);
f.render_widget(chart, chunks[0]); f.render_widget(chart, chunks[0]);
let datasets = [Dataset::default() let datasets = vec![Dataset::default()
.name("data") .name("data")
.marker(symbols::Marker::Braille) .marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow)) .style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line) .graph_type(GraphType::Line)
.data(&DATA)]; .data(&DATA)];
let chart = Chart::default() let chart = Chart::new(datasets)
.block( .block(
Block::default() Block::default()
.title("Chart 2") .title(Span::styled(
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD)) "Chart 2",
StyleDiff::default()
.fg(Color::Cyan)
.modifier(Modifier::BOLD),
))
.borders(Borders::ALL), .borders(Borders::ALL),
) )
.x_axis( .x_axis(
Axis::default() Axis::default()
.title("X Axis") .title("X Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0]) .bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]), .labels(vec![
Span::styled("0", StyleDiff::default().modifier(Modifier::BOLD)),
Span::raw("2.5"),
Span::styled("5.0", StyleDiff::default().modifier(Modifier::BOLD)),
]),
) )
.y_axis( .y_axis(
Axis::default() Axis::default()
.title("Y Axis") .title("Y Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0]) .bounds([0.0, 5.0])
.labels(&["0", "2.5", "5.0"]), .labels(vec![
) Span::styled("0", StyleDiff::default().modifier(Modifier::BOLD)),
.datasets(&datasets); Span::raw("2.5"),
Span::styled("5.0", StyleDiff::default().modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]); f.render_widget(chart, chunks[1]);
let datasets = [Dataset::default() let datasets = vec![Dataset::default()
.name("data") .name("data")
.marker(symbols::Marker::Braille) .marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Yellow)) .style(Style::default().fg(Color::Yellow))
.graph_type(GraphType::Line) .graph_type(GraphType::Line)
.data(&DATA2)]; .data(&DATA2)];
let chart = Chart::default() let chart = Chart::new(datasets)
.block( .block(
Block::default() Block::default()
.title("Chart 3") .title(Span::styled(
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD)) "Chart 3",
StyleDiff::default()
.fg(Color::Cyan)
.modifier(Modifier::BOLD),
))
.borders(Borders::ALL), .borders(Borders::ALL),
) )
.x_axis( .x_axis(
Axis::default() Axis::default()
.title("X Axis") .title("X Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 50.0]) .bounds([0.0, 50.0])
.labels(&["0", "25", "50"]), .labels(vec![
Span::styled("0", StyleDiff::default().modifier(Modifier::BOLD)),
Span::raw("25"),
Span::styled("50", StyleDiff::default().modifier(Modifier::BOLD)),
]),
) )
.y_axis( .y_axis(
Axis::default() Axis::default()
.title("Y Axis") .title("Y Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([0.0, 5.0]) .bounds([0.0, 5.0])
.labels(&["0", "2.5", "5"]), .labels(vec![
) Span::styled("0", StyleDiff::default().modifier(Modifier::BOLD)),
.datasets(&datasets); Span::raw("2.5"),
Span::styled("5", StyleDiff::default().modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[2]); f.render_widget(chart, chunks[2]);
})?; })?;

@ -1,12 +1,13 @@
use tui::{ use tui::{
backend::Backend, backend::Backend,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style, StyleDiff},
symbols, symbols,
text::{Span, Spans},
widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle}, widgets::canvas::{Canvas, Line, Map, MapResolution, Rectangle},
widgets::{ widgets::{
Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, Paragraph, Row, Sparkline, Axis, BarChart, Block, Borders, Chart, Dataset, Gauge, List, ListItem, Paragraph, Row,
Table, Tabs, Text, Wrap, Sparkline, Table, Tabs, Wrap,
}, },
Frame, Frame,
}; };
@ -17,11 +18,21 @@ pub fn draw<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let chunks = Layout::default() let chunks = Layout::default()
.constraints([Constraint::Length(3), Constraint::Min(0)].as_ref()) .constraints([Constraint::Length(3), Constraint::Min(0)].as_ref())
.split(f.size()); .split(f.size());
let tabs = Tabs::default() let titles = app
.tabs
.titles
.iter()
.map(|t| {
Spans::from(vec![Span::styled(
*t,
StyleDiff::default().fg(Color::Green),
)])
})
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title(app.title)) .block(Block::default().borders(Borders::ALL).title(app.title))
.titles(&app.tabs.titles)
.style(Style::default().fg(Color::Green)) .style(Style::default().fg(Color::Green))
.highlight_style(Style::default().fg(Color::Yellow)) .highlight_style_diff(StyleDiff::default().fg(Color::Yellow))
.select(app.tabs.index); .select(app.tabs.index);
f.render_widget(tabs, chunks[0]); f.render_widget(tabs, chunks[0]);
match app.tabs.index { match app.tabs.index {
@ -70,7 +81,7 @@ where
.bg(Color::Black) .bg(Color::Black)
.modifier(Modifier::ITALIC | Modifier::BOLD), .modifier(Modifier::ITALIC | Modifier::BOLD),
) )
.label(&label) .label(label)
.ratio(app.progress); .ratio(app.progress);
f.render_widget(gauge, chunks[0]); f.render_widget(gauge, chunks[0]);
@ -110,29 +121,41 @@ where
.split(chunks[0]); .split(chunks[0]);
// Draw tasks // Draw tasks
let tasks = app.tasks.items.iter().map(|i| Text::raw(*i)); let tasks: Vec<ListItem> = app
.tasks
.items
.iter()
.map(|i| ListItem::new(vec![Spans::from(Span::raw(*i))]))
.collect();
let tasks = List::new(tasks) let tasks = List::new(tasks)
.block(Block::default().borders(Borders::ALL).title("List")) .block(Block::default().borders(Borders::ALL).title("List"))
.highlight_style(Style::default().fg(Color::Yellow).modifier(Modifier::BOLD)) .highlight_style_diff(StyleDiff::default().modifier(Modifier::BOLD))
.highlight_symbol("> "); .highlight_symbol("> ");
f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state); f.render_stateful_widget(tasks, chunks[0], &mut app.tasks.state);
// Draw logs // Draw logs
let info_style = Style::default().fg(Color::White); let info_style = StyleDiff::default().fg(Color::Blue);
let warning_style = Style::default().fg(Color::Yellow); let warning_style = StyleDiff::default().fg(Color::Yellow);
let error_style = Style::default().fg(Color::Magenta); let error_style = StyleDiff::default().fg(Color::Magenta);
let critical_style = Style::default().fg(Color::Red); let critical_style = StyleDiff::default().fg(Color::Red);
let logs = app.logs.items.iter().map(|&(evt, level)| { let logs: Vec<ListItem> = app
Text::styled( .logs
format!("{}: {}", level, evt), .items
match level { .iter()
.map(|&(evt, level)| {
let s = match level {
"ERROR" => error_style, "ERROR" => error_style,
"CRITICAL" => critical_style, "CRITICAL" => critical_style,
"WARNING" => warning_style, "WARNING" => warning_style,
_ => info_style, _ => info_style,
}, };
) let content = vec![Spans::from(vec![
}); Span::styled(format!("{:<9}", level), s),
Span::raw(evt),
])];
ListItem::new(content)
})
.collect();
let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List")); let logs = List::new(logs).block(Block::default().borders(Borders::ALL).title("List"));
f.render_stateful_widget(logs, chunks[1], &mut app.logs.state); f.render_stateful_widget(logs, chunks[1], &mut app.logs.state);
} }
@ -158,12 +181,21 @@ where
f.render_widget(barchart, chunks[1]); f.render_widget(barchart, chunks[1]);
} }
if app.show_chart { if app.show_chart {
let x_labels = [ let x_labels = vec![
format!("{}", app.signals.window[0]), Span::styled(
format!("{}", (app.signals.window[0] + app.signals.window[1]) / 2.0), format!("{}", app.signals.window[0]),
format!("{}", app.signals.window[1]), StyleDiff::default().modifier(Modifier::BOLD),
),
Span::raw(format!(
"{}",
(app.signals.window[0] + app.signals.window[1]) / 2.0
)),
Span::styled(
format!("{}", app.signals.window[1]),
StyleDiff::default().modifier(Modifier::BOLD),
),
]; ];
let datasets = [ let datasets = vec![
Dataset::default() Dataset::default()
.name("data2") .name("data2")
.marker(symbols::Marker::Dot) .marker(symbols::Marker::Dot)
@ -179,30 +211,35 @@ where
.style(Style::default().fg(Color::Yellow)) .style(Style::default().fg(Color::Yellow))
.data(&app.signals.sin2.points), .data(&app.signals.sin2.points),
]; ];
let chart = Chart::default() let chart = Chart::new(datasets)
.block( .block(
Block::default() Block::default()
.title("Chart") .title(Span::styled(
.title_style(Style::default().fg(Color::Cyan).modifier(Modifier::BOLD)) "Chart",
StyleDiff::default()
.fg(Color::Cyan)
.modifier(Modifier::BOLD),
))
.borders(Borders::ALL), .borders(Borders::ALL),
) )
.x_axis( .x_axis(
Axis::default() Axis::default()
.title("X Axis") .title("X Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds(app.signals.window) .bounds(app.signals.window)
.labels(&x_labels), .labels(x_labels),
) )
.y_axis( .y_axis(
Axis::default() Axis::default()
.title("Y Axis") .title("Y Axis")
.style(Style::default().fg(Color::Gray)) .style(Style::default().fg(Color::Gray))
.labels_style(Style::default().modifier(Modifier::ITALIC))
.bounds([-20.0, 20.0]) .bounds([-20.0, 20.0])
.labels(&["-20", "0", "20"]), .labels(vec![
) Span::styled("-20", StyleDiff::default().modifier(Modifier::BOLD)),
.datasets(&datasets); Span::raw("0"),
Span::styled("20", StyleDiff::default().modifier(Modifier::BOLD)),
]),
);
f.render_widget(chart, chunks[1]); f.render_widget(chart, chunks[1]);
} }
} }
@ -211,30 +248,40 @@ fn draw_text<B>(f: &mut Frame<B>, area: Rect)
where where
B: Backend, B: Backend,
{ {
let text = [ let text = vec![
Text::raw("This is a paragraph with several lines. You can change style your text the way you want.\n\nFor example: "), Spans::from("This is a paragraph with several lines. You can change style your text the way you want"),
Text::styled("under", Style::default().fg(Color::Red)), Spans::from(""),
Text::raw(" "), Spans::from(vec![
Text::styled("the", Style::default().fg(Color::Green)), Span::from("For example: "),
Text::raw(" "), Span::styled("under", StyleDiff::default().fg(Color::Red)),
Text::styled("rainbow", Style::default().fg(Color::Blue)), Span::raw(" "),
Text::raw(".\nOh and if you didn't "), Span::styled("the", StyleDiff::default().fg(Color::Green)),
Text::styled("notice", Style::default().modifier(Modifier::ITALIC)), Span::raw(" "),
Text::raw(" you can "), Span::styled("rainbow", StyleDiff::default().fg(Color::Blue)),
Text::styled("automatically", Style::default().modifier(Modifier::BOLD)), Span::raw("."),
Text::raw(" "), ]),
Text::styled("wrap", Style::default().modifier(Modifier::REVERSED)), Spans::from(vec![
Text::raw(" your "), Span::raw("Oh and if you didn't "),
Text::styled("text", Style::default().modifier(Modifier::UNDERLINED)), Span::styled("notice", StyleDiff::default().modifier(Modifier::ITALIC)),
Text::raw(".\nOne more thing is that it should display unicode characters: 10€") Span::raw(" you can "),
Span::styled("automatically", StyleDiff::default().modifier(Modifier::BOLD)),
Span::raw(" "),
Span::styled("wrap", StyleDiff::default().modifier(Modifier::REVERSED)),
Span::raw(" your "),
Span::styled("text", StyleDiff::default().modifier(Modifier::UNDERLINED)),
Span::raw(".")
]),
Spans::from(
"One more thing is that it should display unicode characters: 10€"
),
]; ];
let block = Block::default() let block = Block::default().borders(Borders::ALL).title(Span::styled(
.borders(Borders::ALL) "Footer",
.title("Footer") StyleDiff::default()
.title_style(Style::default().fg(Color::Magenta).modifier(Modifier::BOLD)); .fg(Color::Magenta)
let paragraph = Paragraph::new(text.iter()) .modifier(Modifier::BOLD),
.block(block) ));
.wrap(Wrap { trim: true }); let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
f.render_widget(paragraph, area); f.render_widget(paragraph, area);
} }

@ -89,7 +89,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.block(Block::default().title("Gauge2").borders(Borders::ALL)) .block(Block::default().title("Gauge2").borders(Borders::ALL))
.style(Style::default().fg(Color::Magenta).bg(Color::Green)) .style(Style::default().fg(Color::Magenta).bg(Color::Green))
.percent(app.progress2) .percent(app.progress2)
.label(&label); .label(label);
f.render_widget(gauge, chunks[1]); f.render_widget(gauge, chunks[1]);
let gauge = Gauge::default() let gauge = Gauge::default()
@ -103,7 +103,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.block(Block::default().title("Gauge4").borders(Borders::ALL)) .block(Block::default().title("Gauge4").borders(Borders::ALL))
.style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC)) .style(Style::default().fg(Color::Cyan).modifier(Modifier::ITALIC))
.percent(app.progress4) .percent(app.progress4)
.label(&label); .label(label);
f.render_widget(gauge, chunks[3]); f.render_widget(gauge, chunks[3]);
})?; })?;

@ -10,27 +10,46 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Corner, Direction, Layout}, layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style, StyleDiff},
widgets::{Block, Borders, List, Text}, text::{Span, Spans},
widgets::{Block, Borders, List, ListItem},
Terminal, Terminal,
}; };
struct App<'a> { struct App<'a> {
items: StatefulList<&'a str>, items: StatefulList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>, events: Vec<(&'a str, &'a str)>,
info_style: Style,
warning_style: Style,
error_style: Style,
critical_style: Style,
} }
impl<'a> App<'a> { impl<'a> App<'a> {
fn new() -> App<'a> { fn new() -> App<'a> {
App { App {
items: StatefulList::with_items(vec![ items: StatefulList::with_items(vec![
"Item0", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8", ("Item0", 1),
"Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16", ("Item1", 2),
"Item17", "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24", ("Item2", 1),
("Item3", 3),
("Item4", 1),
("Item5", 4),
("Item6", 1),
("Item7", 3),
("Item8", 1),
("Item9", 6),
("Item10", 1),
("Item11", 3),
("Item12", 1),
("Item13", 2),
("Item14", 1),
("Item15", 1),
("Item16", 4),
("Item17", 1),
("Item18", 5),
("Item19", 4),
("Item20", 1),
("Item21", 2),
("Item22", 1),
("Item23", 3),
("Item24", 1),
]), ]),
events: vec![ events: vec![
("Event1", "INFO"), ("Event1", "INFO"),
@ -60,10 +79,6 @@ impl<'a> App<'a> {
("Event25", "INFO"), ("Event25", "INFO"),
("Event26", "INFO"), ("Event26", "INFO"),
], ],
info_style: Style::default().fg(Color::White),
warning_style: Style::default().fg(Color::Yellow),
error_style: Style::default().fg(Color::Magenta),
critical_style: Style::default().fg(Color::Red),
} }
} }
@ -96,25 +111,60 @@ fn main() -> Result<(), Box<dyn Error>> {
let style = Style::default().fg(Color::Black).bg(Color::White); let style = Style::default().fg(Color::Black).bg(Color::White);
let items = app.items.items.iter().map(|i| Text::raw(*i)); let items: Vec<ListItem> = app
.items
.items
.iter()
.map(|i| {
let mut lines = vec![Spans::from(i.0)];
for _ in 0..i.1 {
lines.push(Spans::from(Span::styled(
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
StyleDiff::default().modifier(Modifier::ITALIC),
)));
}
ListItem::new(lines)
})
.collect();
let items = List::new(items) let items = List::new(items)
.block(Block::default().borders(Borders::ALL).title("List")) .block(Block::default().borders(Borders::ALL).title("List"))
.style(style) .style(style)
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD)) .highlight_style_diff(
.highlight_symbol(">"); StyleDiff::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(items, chunks[0], &mut app.items.state); f.render_stateful_widget(items, chunks[0], &mut app.items.state);
let events = app.events.iter().map(|&(evt, level)| { let events: Vec<ListItem> = app
Text::styled( .events
format!("{}: {}", level, evt), .iter()
match level { .map(|&(evt, level)| {
"ERROR" => app.error_style, let s = match level {
"CRITICAL" => app.critical_style, "CRITICAL" => StyleDiff::default().fg(Color::Red),
"WARNING" => app.warning_style, "ERROR" => StyleDiff::default().fg(Color::Magenta),
_ => app.info_style, "WARNING" => StyleDiff::default().fg(Color::Yellow),
}, "INFO" => StyleDiff::default().fg(Color::Blue),
) _ => StyleDiff::default(),
}); };
let header = Spans::from(vec![
Span::styled(format!("{:<9}", level), s),
Span::raw(" "),
Span::styled(
"2020-01-01 10:00:00",
StyleDiff::default().modifier(Modifier::ITALIC),
),
]);
let log = Spans::from(vec![Span::raw(evt)]);
ListItem::new(vec![
Spans::from("-".repeat(chunks[1].width as usize)),
header,
Spans::from(""),
log,
])
})
.collect();
let events_list = List::new(events) let events_list = List::new(events)
.block(Block::default().borders(Borders::ALL).title("List")) .block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft); .start_corner(Corner::BottomLeft);

@ -7,8 +7,9 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout}, layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style, StyleDiff},
widgets::{Block, Borders, Paragraph, Text, Wrap}, text::{Span, Spans},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal, Terminal,
}; };
@ -51,41 +52,43 @@ fn main() -> Result<(), Box<dyn Error>> {
) )
.split(size); .split(size);
let text = [ let text = vec![
Text::raw("This is a line \n"), Spans::from("This is a line "),
Text::styled("This is a line \n", Style::default().fg(Color::Red)), Spans::from(Span::styled("This is a line ", StyleDiff::default().fg(Color::Red))),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)), Spans::from(Span::styled("This is a line", StyleDiff::default().bg(Color::Blue))),
Text::styled( Spans::from(Span::styled(
"This is a longer line\n", "This is a longer line",
Style::default().modifier(Modifier::CROSSED_OUT), StyleDiff::default().modifier(Modifier::CROSSED_OUT),
), )),
Text::styled(&long_line, Style::default().bg(Color::Green)), Spans::from(Span::styled(&long_line, StyleDiff::default().bg(Color::Green))),
Text::styled( Spans::from(Span::styled(
"This is a line\n", "This is a line",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC), StyleDiff::default().fg(Color::Green).modifier(Modifier::ITALIC),
), )),
]; ];
let block = Block::default() let create_block = |title| {
.borders(Borders::ALL) Block::default()
.title_style(Style::default().modifier(Modifier::BOLD)); .borders(Borders::ALL)
let paragraph = Paragraph::new(text.iter()) .title(Span::styled(title, StyleDiff::default().add_modifier(Modifier::BOLD)))
.block(block.clone().title("Left, no wrap")) };
let paragraph = Paragraph::new(text.clone())
.block(create_block("Left, no wrap"))
.alignment(Alignment::Left); .alignment(Alignment::Left);
f.render_widget(paragraph, chunks[0]); f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text.clone())
.block(block.clone().title("Left, wrap")) .block(create_block("Left, wrap"))
.alignment(Alignment::Left) .alignment(Alignment::Left)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]); f.render_widget(paragraph, chunks[1]);
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text.clone())
.block(block.clone().title("Center, wrap")) .block(create_block("Center, wrap"))
.alignment(Alignment::Center) .alignment(Alignment::Center)
.wrap(Wrap { trim: true }) .wrap(Wrap { trim: true })
.scroll((scroll, 0)); .scroll((scroll, 0));
f.render_widget(paragraph, chunks[2]); f.render_widget(paragraph, chunks[2]);
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text)
.block(block.clone().title("Right, wrap")) .block(create_block("Right, wrap"))
.alignment(Alignment::Right) .alignment(Alignment::Right)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[3]); f.render_widget(paragraph, chunks[3]);

@ -4,13 +4,12 @@ mod util;
use crate::util::event::{Event, Events}; use crate::util::event::{Event, Events};
use std::{error::Error, io}; use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen}; use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::layout::Rect;
use tui::widgets::Clear;
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout}, layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, StyleDiff},
widgets::{Block, Borders, Paragraph, Text, Wrap}, text::{Span, Spans},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Terminal, Terminal,
}; };
@ -66,27 +65,27 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300); let mut long_line = s.repeat(usize::from(size.width)*usize::from(size.height)/300);
long_line.push('\n'); long_line.push('\n');
let text = [ let text = vec![
Text::raw("This is a line \n"), Spans::from("This is a line "),
Text::styled("This is a line \n", Style::default().fg(Color::Red)), Spans::from(Span::styled("This is a line ", StyleDiff::default().fg(Color::Red))),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)), Spans::from(Span::styled("This is a line", StyleDiff::default().bg(Color::Blue))),
Text::styled( Spans::from(Span::styled(
"This is a longer line\n", "This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT), StyleDiff::default().modifier(Modifier::CROSSED_OUT),
), )),
Text::styled(&long_line, Style::default().bg(Color::Green)), Spans::from(Span::styled(&long_line, StyleDiff::default().bg(Color::Green))),
Text::styled( Spans::from(Span::styled(
"This is a line\n", "This is a line\n",
Style::default().fg(Color::Green).modifier(Modifier::ITALIC), StyleDiff::default().fg(Color::Green).modifier(Modifier::ITALIC),
), )),
]; ];
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text.clone())
.block(Block::default().title("Left Block").borders(Borders::ALL)) .block(Block::default().title("Left Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(Wrap { trim: true }); .alignment(Alignment::Left).wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[0]); f.render_widget(paragraph, chunks[0]);
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text)
.block(Block::default().title("Right Block").borders(Borders::ALL)) .block(Block::default().title("Right Block").borders(Borders::ALL))
.alignment(Alignment::Left).wrap(Wrap { trim: true }); .alignment(Alignment::Left).wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]); f.render_widget(paragraph, chunks[1]);

@ -10,7 +10,8 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Style}, style::{Color, Modifier, Style, StyleDiff},
text::{Span, Spans},
widgets::{Block, Borders, Tabs}, widgets::{Block, Borders, Tabs},
Terminal, Terminal,
}; };
@ -47,12 +48,23 @@ fn main() -> Result<(), Box<dyn Error>> {
let block = Block::default().style(Style::default().bg(Color::White)); let block = Block::default().style(Style::default().bg(Color::White));
f.render_widget(block, size); f.render_widget(block, size);
let tabs = Tabs::default() let titles = app
.tabs
.titles
.iter()
.map(|t| {
let (first, rest) = t.split_at(1);
Spans::from(vec![
Span::styled(first, StyleDiff::default().fg(Color::Yellow)),
Span::styled(rest, StyleDiff::default().fg(Color::Green)),
])
})
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Tabs")) .block(Block::default().borders(Borders::ALL).title("Tabs"))
.titles(&app.tabs.titles)
.select(app.tabs.index) .select(app.tabs.index)
.style(Style::default().fg(Color::Cyan)) .style(Style::default().fg(Color::Cyan))
.highlight_style(Style::default().fg(Color::Yellow)); .highlight_style_diff(StyleDiff::default().modifier(Modifier::BOLD));
f.render_widget(tabs, chunks[0]); f.render_widget(tabs, chunks[0]);
let inner = match app.tabs.index { let inner = match app.tabs.index {
0 => Block::default().title("Inner 0").borders(Borders::ALL), 0 => Block::default().title("Inner 0").borders(Borders::ALL),

@ -19,8 +19,9 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{ use tui::{
backend::TermionBackend, backend::TermionBackend,
layout::{Constraint, Direction, Layout}, layout::{Constraint, Direction, Layout},
style::{Color, Style}, style::{Color, Modifier, Style, StyleDiff},
widgets::{Block, Borders, List, Paragraph, Text}, text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, Paragraph},
Terminal, Terminal,
}; };
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -81,15 +82,27 @@ fn main() -> Result<(), Box<dyn Error>> {
.split(f.size()); .split(f.size());
let msg = match app.input_mode { let msg = match app.input_mode {
InputMode::Normal => "Press q to exit, e to start editing.", InputMode::Normal => vec![
InputMode::Editing => "Press Esc to stop editing, Enter to record the message", Span::raw("Press "),
Span::styled("q", StyleDiff::default().add_modifier(Modifier::BOLD)),
Span::raw(" to exit, "),
Span::styled("e", StyleDiff::default().add_modifier(Modifier::BOLD)),
Span::raw(" to start editing."),
],
InputMode::Editing => vec![
Span::raw("Press "),
Span::styled("Esc", StyleDiff::default().add_modifier(Modifier::BOLD)),
Span::raw(" to stop editing, "),
Span::styled("Enter", StyleDiff::default().add_modifier(Modifier::BOLD)),
Span::raw(" to record the message"),
],
}; };
let text = [Text::raw(msg)]; let text = vec![Spans::from(msg)];
let help_message = Paragraph::new(text.iter()); let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]); f.render_widget(help_message, chunks[0]);
let text = [Text::raw(&app.input)]; let text = vec![Spans::from(app.input.as_ref())];
let input = Paragraph::new(text.iter()) let input = Paragraph::new(text)
.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"));
f.render_widget(input, chunks[1]); f.render_widget(input, chunks[1]);
@ -109,11 +122,15 @@ fn main() -> Result<(), Box<dyn Error>> {
} }
} }
let messages = app let messages: Vec<ListItem> = app
.messages .messages
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, m)| Text::raw(format!("{}: {}", i, m))); .map(|(i, m)| {
let content = vec![Spans::from(Span::raw(format!("{}: {}", i, m)))];
ListItem::new(content)
})
.collect();
let messages = let messages =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages")); List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]); f.render_widget(messages, chunks[2]);

@ -1,6 +1,7 @@
use crate::{ use crate::{
layout::Rect, layout::Rect,
style::{Color, Modifier, Style}, style::{Color, Modifier, Style},
text::{Span, Spans},
}; };
use std::cmp::min; use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@ -298,6 +299,51 @@ impl Buffer {
(x_offset as u16, y) (x_offset as u16, y)
} }
pub fn set_spans<'a>(
&mut self,
x: u16,
y: u16,
spans: &Spans<'a>,
width: u16,
base_style: Style,
) -> (u16, u16) {
let mut remaining_width = width;
let mut x = x;
for span in &spans.0 {
if remaining_width == 0 {
break;
}
let pos = self.set_stringn(
x,
y,
span.content.as_ref(),
remaining_width as usize,
base_style.patch(span.style_diff),
);
let w = pos.0.saturating_sub(x);
x = pos.0;
remaining_width = remaining_width.saturating_sub(w);
}
(x, y)
}
pub fn set_span<'a>(
&mut self,
x: u16,
y: u16,
span: &Span<'a>,
width: u16,
base_style: Style,
) -> (u16, u16) {
self.set_stringn(
x,
y,
span.content.as_ref(),
width as usize,
base_style.patch(span.style_diff),
)
}
pub fn set_background(&mut self, area: Rect, color: Color) { pub fn set_background(&mut self, area: Rect, color: Color) {
for y in area.top()..area.bottom() { for y in area.top()..area.bottom() {
for x in area.left()..area.right() { for x in area.left()..area.right() {

@ -151,6 +151,7 @@ pub mod layout;
pub mod style; pub mod style;
pub mod symbols; pub mod symbols;
pub mod terminal; pub mod terminal;
pub mod text;
pub mod widgets; pub mod widgets;
pub use self::terminal::{Frame, Terminal}; pub use self::terminal::{Frame, Terminal};

@ -83,14 +83,17 @@ where
/// # use tui::Terminal; /// # use tui::Terminal;
/// # use tui::backend::TermionBackend; /// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect; /// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListState, Text}; /// # use tui::widgets::{List, ListItem, ListState};
/// # let stdout = io::stdout(); /// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout); /// # let backend = TermionBackend::new(stdout);
/// # let mut terminal = Terminal::new(backend).unwrap(); /// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default(); /// let mut state = ListState::default();
/// state.select(Some(1)); /// state.select(Some(1));
/// let items = vec![Text::raw("Item 1"), Text::raw("Item 2")]; /// let items = vec![
/// let list = List::new(items.into_iter()); /// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ];
/// let list = List::new(items);
/// let area = Rect::new(0, 0, 5, 5); /// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame(); /// let mut frame = terminal.get_frame();
/// frame.render_stateful_widget(list, area, &mut state); /// frame.render_stateful_widget(list, area, &mut state);

@ -0,0 +1,307 @@
//! Primitives for styled text.
//!
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//!
//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Spans`].
//!
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
//! that you can start by using simple `String` or `&str` and then promote them to the previous
//! primitives when you need additional styling capabilities.
//!
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
//! its `title` property (which is a [`Spans`] under the hood):
//!
//! ```rust
//! # use tui::widgets::Block;
//! # use tui::text::{Span, Spans};
//! # use tui::style::{Color, StyleDiff};
//! // A simple string with no styling.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style_diff: StyleDiff { .. } }
//! // ])
//! let block = Block::default().title("My title");
//!
//! // A simple string with a unique style.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style_diff: StyleDiff { fg: Some(Color::Yellow), .. }
//! // ])
//! let block = Block::default().title(
//! Span::styled("My title", StyleDiff::default().fg(Color::Yellow))
//! );
//!
//! // A string with multiple styles.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My"), style_diff: StyleDiff { fg: Some(Color::Yellow), .. } },
//! // Span { content: Cow::Borrowed(" title"), .. }
//! // ])
//! let block = Block::default().title(vec![
//! Span::styled("My", StyleDiff::default().fg(Color::Yellow)),
//! Span::raw(" title"),
//! ]);
//! ```
use crate::style::{Style, StyleDiff};
use std::{borrow::Cow, cmp::max};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
/// A grapheme associated to a style.
#[derive(Debug, Clone, PartialEq)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,
}
/// A string where all graphemes have the same style.
#[derive(Debug, Clone, PartialEq)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style_diff: StyleDiff,
}
impl<'a> Span<'a> {
/// Create a span with no style.
///
/// ## Examples
///
/// ```rust
/// # use tui::text::Span;
/// Span::raw("My text");
/// Span::raw(String::from("My text"));
/// ```
pub fn raw<T>(content: T) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style_diff: StyleDiff::default(),
}
}
/// Create a span with a style.
///
/// # Examples
///
/// ```rust
/// # use tui::text::Span;
/// # use tui::style::{Color, Modifier, StyleDiff};
/// let style = StyleDiff::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
/// ```
pub fn styled<T>(content: T, style_diff: StyleDiff) -> Span<'a>
where
T: Into<Cow<'a, str>>,
{
Span {
content: content.into(),
style_diff,
}
}
/// Returns the width of the content held by this span.
pub fn width(&self) -> usize {
self.content.width()
}
/// Returns an iterator over the graphemes held by this span.
///
/// `base_style` is the [`Style`] that will be patched with each grapheme [`StyleDiff`] to get
/// the resulting [`Style`].
///
/// ## Examples
///
/// ```rust
/// # use tui::text::{Span, StyledGrapheme};
/// # use tui::style::{Color, Modifier, Style, StyleDiff};
/// # use std::iter::Iterator;
/// let style_diff = StyleDiff::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style_diff);
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
/// let styled_graphemes = span.styled_graphemes(style);
/// assert_eq!(
/// vec![
/// StyledGrapheme {
/// symbol: "T",
/// style: Style {
/// fg: Color::Yellow,
/// bg: Color::Black,
/// modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "e",
/// style: Style {
/// fg: Color::Yellow,
/// bg: Color::Black,
/// modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "x",
/// style: Style {
/// fg: Color::Yellow,
/// bg: Color::Black,
/// modifier: Modifier::empty(),
/// },
/// },
/// StyledGrapheme {
/// symbol: "t",
/// style: Style {
/// fg: Color::Yellow,
/// bg: Color::Black,
/// modifier: Modifier::empty(),
/// },
/// },
/// ],
/// styled_graphemes.collect::<Vec<StyledGrapheme>>()
/// );
/// ```
pub fn styled_graphemes(
&'a self,
base_style: Style,
) -> impl Iterator<Item = StyledGrapheme<'a>> {
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
.map(move |g| StyledGrapheme {
symbol: g,
style: base_style.patch(self.style_diff),
})
.filter(|s| s.symbol != "\n")
}
}
impl<'a> From<String> for Span<'a> {
fn from(s: String) -> Span<'a> {
Span::raw(s)
}
}
impl<'a> From<&'a str> for Span<'a> {
fn from(s: &'a str) -> Span<'a> {
Span::raw(s)
}
}
/// A string composed of clusters of graphemes, each with their own style.
#[derive(Debug, Clone, PartialEq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
impl<'a> Default for Spans<'a> {
fn default() -> Spans<'a> {
Spans(Vec::new())
}
}
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
/// ## Examples
///
/// ```rust
/// # use tui::text::{Span, Spans};
/// # use tui::style::{Color, StyleDiff};
/// let spans = Spans::from(vec![
/// Span::styled("My", StyleDiff::default().fg(Color::Yellow)),
/// Span::raw(" text"),
/// ]);
/// assert_eq!(7, spans.width());
/// ```
pub fn width(&self) -> usize {
self.0.iter().fold(0, |acc, s| acc + s.width())
}
}
impl<'a> From<String> for Spans<'a> {
fn from(s: String) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Spans<'a> {
fn from(s: &'a str) -> Spans<'a> {
Spans(vec![Span::from(s)])
}
}
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
Spans(spans)
}
}
impl<'a> From<Span<'a>> for Spans<'a> {
fn from(span: Span<'a>) -> Spans<'a> {
Spans(vec![span])
}
}
impl<'a> From<Spans<'a>> for String {
fn from(line: Spans<'a>) -> String {
line.0.iter().fold(String::new(), |mut acc, s| {
acc.push_str(s.content.as_ref());
acc
})
}
}
/// A string split over multiple lines where each line is composed of several clusters, each with
/// their own style.
#[derive(Debug, Clone, PartialEq)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
}
impl<'a> Default for Text<'a> {
fn default() -> Text<'a> {
Text { lines: Vec::new() }
}
}
impl<'a> Text<'a> {
/// Returns the max width of all the lines.
///
/// ## Examples
///
/// ```rust
/// use tui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(15, text.width());
/// ```
pub fn width(&self) -> usize {
self.lines.iter().fold(0, |acc, l| max(acc, l.width()))
}
/// Returns the height.
///
/// ## Examples
///
/// ```rust
/// use tui::text::Text;
/// let text = Text::from("The first line\nThe second line");
/// assert_eq!(2, text.height());
/// ```
pub fn height(&self) -> usize {
self.lines.len()
}
}
impl<'a> From<&'a str> for Text<'a> {
fn from(s: &'a str) -> Text<'a> {
Text {
lines: s.lines().map(Spans::from).collect(),
}
}
}
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
Text { lines }
}
}

@ -120,10 +120,11 @@ impl<'a> BarChart<'a> {
impl<'a> Widget for BarChart<'a> { impl<'a> Widget for BarChart<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block { let chart_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };

@ -1,8 +1,11 @@
use crate::buffer::Buffer; use crate::{
use crate::layout::Rect; buffer::Buffer,
use crate::style::Style; layout::Rect,
use crate::symbols::line; style::{Style, StyleDiff},
use crate::widgets::{Borders, Widget}; symbols::line,
text::{Span, Spans},
widgets::{Borders, Widget},
};
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum BorderType { pub enum BorderType {
@ -33,18 +36,15 @@ impl BorderType {
/// # use tui::style::{Style, Color}; /// # use tui::style::{Style, Color};
/// Block::default() /// Block::default()
/// .title("Block") /// .title("Block")
/// .title_style(Style::default().fg(Color::Red))
/// .borders(Borders::LEFT | Borders::RIGHT) /// .borders(Borders::LEFT | Borders::RIGHT)
/// .border_style(Style::default().fg(Color::White)) /// .border_style(Style::default().fg(Color::White))
/// .border_type(BorderType::Rounded) /// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black)); /// .style(Style::default().bg(Color::Black));
/// ``` /// ```
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub struct Block<'a> { pub struct Block<'a> {
/// Optional title place on the upper left of the block /// Optional title place on the upper left of the block
title: Option<&'a str>, title: Option<Spans<'a>>,
/// Title style
title_style: Style,
/// Visible borders /// Visible borders
borders: Borders, borders: Borders,
/// Border style /// Border style
@ -60,7 +60,6 @@ impl<'a> Default for Block<'a> {
fn default() -> Block<'a> { fn default() -> Block<'a> {
Block { Block {
title: None, title: None,
title_style: Default::default(),
borders: Borders::NONE, borders: Borders::NONE,
border_style: Default::default(), border_style: Default::default(),
border_type: BorderType::Plain, border_type: BorderType::Plain,
@ -70,13 +69,23 @@ impl<'a> Default for Block<'a> {
} }
impl<'a> Block<'a> { impl<'a> Block<'a> {
pub fn title(mut self, title: &'a str) -> Block<'a> { pub fn title<T>(mut self, title: T) -> Block<'a>
self.title = Some(title); where
T: Into<Spans<'a>>,
{
self.title = Some(title.into());
self self
} }
#[deprecated(
since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Block<'a> { pub fn title_style(mut self, style: Style) -> Block<'a> {
self.title_style = style; if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, StyleDiff::from(style))));
}
self self
} }
@ -199,13 +208,7 @@ impl<'a> Widget for Block<'a> {
0 0
}; };
let width = area.width - lx - rx; let width = area.width - lx - rx;
buf.set_stringn( buf.set_spans(area.left() + lx, area.top(), &title, width, self.style);
area.left() + lx,
area.top(),
title,
width as usize,
self.title_style,
);
} }
} }
} }

@ -419,10 +419,11 @@ where
F: Fn(&mut Context), F: Fn(&mut Context),
{ {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(mut self, area: Rect, buf: &mut Buffer) {
let canvas_area = match self.block { let canvas_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };

@ -1,8 +1,9 @@
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Rect}, layout::{Constraint, Rect},
style::Style, style::{Style, StyleDiff},
symbols, symbols,
text::{Span, Spans},
widgets::{ widgets::{
canvas::{Canvas, Line, Points}, canvas::{Canvas, Line, Points},
Block, Borders, Widget, Block, Borders, Widget,
@ -13,70 +14,60 @@ use unicode_width::UnicodeWidthStr;
/// An X or Y axis for the chart widget /// An X or Y axis for the chart widget
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Axis<'a, L> pub struct Axis<'a> {
where
L: AsRef<str> + 'a,
{
/// Title displayed next to axis end /// Title displayed next to axis end
title: Option<&'a str>, title: Option<Spans<'a>>,
/// Style of the title
title_style: Style,
/// Bounds for the axis (all data points outside these limits will not be represented) /// Bounds for the axis (all data points outside these limits will not be represented)
bounds: [f64; 2], bounds: [f64; 2],
/// A list of labels to put to the left or below the axis /// A list of labels to put to the left or below the axis
labels: Option<&'a [L]>, labels: Option<Vec<Span<'a>>>,
/// The labels' style
labels_style: Style,
/// The style used to draw the axis itself /// The style used to draw the axis itself
style: Style, style: Style,
} }
impl<'a, L> Default for Axis<'a, L> impl<'a> Default for Axis<'a> {
where fn default() -> Axis<'a> {
L: AsRef<str>,
{
fn default() -> Axis<'a, L> {
Axis { Axis {
title: None, title: None,
title_style: Default::default(),
bounds: [0.0, 0.0], bounds: [0.0, 0.0],
labels: None, labels: None,
labels_style: Default::default(),
style: Default::default(), style: Default::default(),
} }
} }
} }
impl<'a, L> Axis<'a, L> impl<'a> Axis<'a> {
where pub fn title<T>(mut self, title: T) -> Axis<'a>
L: AsRef<str>, where
{ T: Into<Spans<'a>>,
pub fn title(mut self, title: &'a str) -> Axis<'a, L> { {
self.title = Some(title); self.title = Some(title.into());
self self
} }
pub fn title_style(mut self, style: Style) -> Axis<'a, L> { #[deprecated(
self.title_style = style; since = "0.10.0",
note = "You should use styling capabilities of `text::Spans` given as argument of the `title` method to apply styling to the title."
)]
pub fn title_style(mut self, style: Style) -> Axis<'a> {
if let Some(t) = self.title {
let title = String::from(t);
self.title = Some(Spans::from(Span::styled(title, StyleDiff::from(style))));
}
self self
} }
pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a, L> { pub fn bounds(mut self, bounds: [f64; 2]) -> Axis<'a> {
self.bounds = bounds; self.bounds = bounds;
self self
} }
pub fn labels(mut self, labels: &'a [L]) -> Axis<'a, L> { pub fn labels(mut self, labels: Vec<Span<'a>>) -> Axis<'a> {
self.labels = Some(labels); self.labels = Some(labels);
self self
} }
pub fn labels_style(mut self, style: Style) -> Axis<'a, L> { pub fn style(mut self, style: Style) -> Axis<'a> {
self.labels_style = style;
self
}
pub fn style(mut self, style: Style) -> Axis<'a, L> {
self.style = style; self.style = style;
self self
} }
@ -192,104 +183,84 @@ impl Default for ChartLayout {
/// ``` /// ```
/// # use tui::symbols; /// # use tui::symbols;
/// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType}; /// # use tui::widgets::{Block, Borders, Chart, Axis, Dataset, GraphType};
/// # use tui::style::{Style, Color}; /// # use tui::style::{Style, StyleDiff, Color};
/// Chart::default() /// # use tui::text::Span;
/// let datasets = vec![
/// Dataset::default()
/// .name("data1")
/// .marker(symbols::Marker::Dot)
/// .graph_type(GraphType::Scatter)
/// .style(Style::default().fg(Color::Cyan))
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
/// Dataset::default()
/// .name("data2")
/// .marker(symbols::Marker::Braille)
/// .graph_type(GraphType::Line)
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)]),
/// ];
/// Chart::new(datasets)
/// .block(Block::default().title("Chart")) /// .block(Block::default().title("Chart"))
/// .x_axis(Axis::default() /// .x_axis(Axis::default()
/// .title("X Axis") /// .title(Span::styled("X Axis", StyleDiff::default().fg(Color::Red)))
/// .title_style(Style::default().fg(Color::Red))
/// .style(Style::default().fg(Color::White)) /// .style(Style::default().fg(Color::White))
/// .bounds([0.0, 10.0]) /// .bounds([0.0, 10.0])
/// .labels(&["0.0", "5.0", "10.0"])) /// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()))
/// .y_axis(Axis::default() /// .y_axis(Axis::default()
/// .title("Y Axis") /// .title(Span::styled("Y Axis", StyleDiff::default().fg(Color::Red)))
/// .title_style(Style::default().fg(Color::Red))
/// .style(Style::default().fg(Color::White)) /// .style(Style::default().fg(Color::White))
/// .bounds([0.0, 10.0]) /// .bounds([0.0, 10.0])
/// .labels(&["0.0", "5.0", "10.0"])) /// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
/// .datasets(&[Dataset::default()
/// .name("data1")
/// .marker(symbols::Marker::Dot)
/// .graph_type(GraphType::Scatter)
/// .style(Style::default().fg(Color::Cyan))
/// .data(&[(0.0, 5.0), (1.0, 6.0), (1.5, 6.434)]),
/// Dataset::default()
/// .name("data2")
/// .marker(symbols::Marker::Braille)
/// .graph_type(GraphType::Line)
/// .style(Style::default().fg(Color::Magenta))
/// .data(&[(4.0, 5.0), (5.0, 8.0), (7.66, 13.5)])]);
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Chart<'a, LX, LY> pub struct Chart<'a> {
where
LX: AsRef<str> + 'a,
LY: AsRef<str> + 'a,
{
/// A block to display around the widget eventually /// A block to display around the widget eventually
block: Option<Block<'a>>, block: Option<Block<'a>>,
/// The horizontal axis /// The horizontal axis
x_axis: Axis<'a, LX>, x_axis: Axis<'a>,
/// The vertical axis /// The vertical axis
y_axis: Axis<'a, LY>, y_axis: Axis<'a>,
/// A reference to the datasets /// A reference to the datasets
datasets: &'a [Dataset<'a>], datasets: Vec<Dataset<'a>>,
/// The widget base style /// The widget base style
style: Style, style: Style,
/// Constraints used to determine whether the legend should be shown or /// Constraints used to determine whether the legend should be shown or not
/// not
hidden_legend_constraints: (Constraint, Constraint), hidden_legend_constraints: (Constraint, Constraint),
} }
impl<'a, LX, LY> Default for Chart<'a, LX, LY> impl<'a> Chart<'a> {
where pub fn new(datasets: Vec<Dataset<'a>>) -> Chart<'a> {
LX: AsRef<str>,
LY: AsRef<str>,
{
fn default() -> Chart<'a, LX, LY> {
Chart { Chart {
block: None, block: None,
x_axis: Axis::default(), x_axis: Axis::default(),
y_axis: Axis::default(), y_axis: Axis::default(),
style: Default::default(), style: Default::default(),
datasets: &[], datasets,
hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)), hidden_legend_constraints: (Constraint::Ratio(1, 4), Constraint::Ratio(1, 4)),
} }
} }
}
impl<'a, LX, LY> Chart<'a, LX, LY> pub fn block(mut self, block: Block<'a>) -> Chart<'a> {
where
LX: AsRef<str>,
LY: AsRef<str>,
{
pub fn block(mut self, block: Block<'a>) -> Chart<'a, LX, LY> {
self.block = Some(block); self.block = Some(block);
self self
} }
pub fn style(mut self, style: Style) -> Chart<'a, LX, LY> { pub fn style(mut self, style: Style) -> Chart<'a> {
self.style = style; self.style = style;
self self
} }
pub fn x_axis(mut self, axis: Axis<'a, LX>) -> Chart<'a, LX, LY> { pub fn x_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.x_axis = axis; self.x_axis = axis;
self self
} }
pub fn y_axis(mut self, axis: Axis<'a, LY>) -> Chart<'a, LX, LY> { pub fn y_axis(mut self, axis: Axis<'a>) -> Chart<'a> {
self.y_axis = axis; self.y_axis = axis;
self self
} }
pub fn datasets(mut self, datasets: &'a [Dataset<'a>]) -> Chart<'a, LX, LY> { /// Set the constraints used to determine whether the legend should be shown or not.
self.datasets = datasets;
self
}
/// Set the constraints used to determine whether the legend should be shown or
/// not.
/// ///
/// # Examples /// # Examples
/// ///
@ -302,13 +273,10 @@ where
/// ); /// );
/// // Hide the legend when either its width is greater than 33% of the total widget width /// // Hide the legend when either its width is greater than 33% of the total widget width
/// // or if its height is greater than 25% of the total widget height. /// // or if its height is greater than 25% of the total widget height.
/// let _chart: Chart<String, String> = Chart::default() /// let _chart: Chart = Chart::new(vec![])
/// .hidden_legend_constraints(constraints); /// .hidden_legend_constraints(constraints);
/// ``` /// ```
pub fn hidden_legend_constraints( pub fn hidden_legend_constraints(mut self, constraints: (Constraint, Constraint)) -> Chart<'a> {
mut self,
constraints: (Constraint, Constraint),
) -> Chart<'a, LX, LY> {
self.hidden_legend_constraints = constraints; self.hidden_legend_constraints = constraints;
self self
} }
@ -328,14 +296,14 @@ where
y -= 1; y -= 1;
} }
if let Some(y_labels) = self.y_axis.labels { if let Some(ref y_labels) = self.y_axis.labels {
let mut max_width = y_labels let mut max_width = y_labels
.iter() .iter()
.fold(0, |acc, l| max(l.as_ref().width(), acc)) .fold(0, |acc, l| max(l.content.width(), acc))
as u16; as u16;
if let Some(x_labels) = self.x_axis.labels { if let Some(ref x_labels) = self.x_axis.labels {
if !x_labels.is_empty() { if !x_labels.is_empty() {
max_width = max(max_width, x_labels[0].as_ref().width() as u16); max_width = max(max_width, x_labels[0].content.width() as u16);
} }
} }
if x + max_width < area.right() { if x + max_width < area.right() {
@ -358,14 +326,14 @@ where
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1); layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
} }
if let Some(title) = self.x_axis.title { if let Some(ref title) = self.x_axis.title {
let w = title.width() as u16; let w = title.width() as u16;
if w < layout.graph_area.width && layout.graph_area.height > 2 { if w < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_x = Some((x + layout.graph_area.width - w, y)); layout.title_x = Some((x + layout.graph_area.width - w, y));
} }
} }
if let Some(title) = self.y_axis.title { if let Some(ref title) = self.y_axis.title {
let w = title.width() as u16; let w = title.width() as u16;
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 { if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
layout.title_y = Some((x + 1, area.top())); layout.title_y = Some((x + 1, area.top()));
@ -399,16 +367,13 @@ where
} }
} }
impl<'a, LX, LY> Widget for Chart<'a, LX, LY> impl<'a> Widget for Chart<'a> {
where
LX: AsRef<str>,
LY: AsRef<str>,
{
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block { let chart_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };
@ -423,26 +388,39 @@ where
if let Some((x, y)) = layout.title_x { if let Some((x, y)) = layout.title_x {
let title = self.x_axis.title.unwrap(); let title = self.x_axis.title.unwrap();
buf.set_string(x, y, title, self.x_axis.title_style); buf.set_spans(
x,
y,
&title,
graph_area.right().saturating_sub(x),
self.style,
);
} }
if let Some((x, y)) = layout.title_y { if let Some((x, y)) = layout.title_y {
let title = self.y_axis.title.unwrap(); let title = self.y_axis.title.unwrap();
buf.set_string(x, y, title, self.y_axis.title_style); buf.set_spans(
x,
y,
&title,
graph_area.right().saturating_sub(x),
self.style,
);
} }
if let Some(y) = layout.label_x { if let Some(y) = layout.label_x {
let labels = self.x_axis.labels.unwrap(); let labels = self.x_axis.labels.unwrap();
let total_width = labels.iter().fold(0, |acc, l| l.as_ref().width() + acc) as u16; let total_width = labels.iter().fold(0, |acc, l| l.content.width() + acc) as u16;
let labels_len = labels.len() as u16; let labels_len = labels.len() as u16;
if total_width < graph_area.width && labels_len > 1 { if total_width < graph_area.width && labels_len > 1 {
for (i, label) in labels.iter().enumerate() { for (i, label) in labels.iter().enumerate() {
buf.set_string( buf.set_span(
graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1) graph_area.left() + i as u16 * (graph_area.width - 1) / (labels_len - 1)
- label.as_ref().width() as u16, - label.content.width() as u16,
y, y,
label.as_ref(), label,
self.x_axis.labels_style, label.width() as u16,
self.style,
); );
} }
} }
@ -454,11 +432,12 @@ where
for (i, label) in labels.iter().enumerate() { for (i, label) in labels.iter().enumerate() {
let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1); let dy = i as u16 * (graph_area.height - 1) / (labels_len - 1);
if dy < graph_area.bottom() { if dy < graph_area.bottom() {
buf.set_string( buf.set_span(
x, x,
graph_area.bottom() - 1 - dy, graph_area.bottom() - 1 - dy,
label.as_ref(), label,
self.y_axis.labels_style, label.width() as u16,
self.style,
); );
} }
} }
@ -488,7 +467,7 @@ where
} }
} }
for dataset in self.datasets { for dataset in &self.datasets {
Canvas::default() Canvas::default()
.background_color(self.style.bg) .background_color(self.style.bg)
.x_bounds(self.x_axis.bounds) .x_bounds(self.x_axis.bounds)
@ -543,12 +522,6 @@ mod tests {
#[test] #[test]
fn it_should_hide_the_legend() { fn it_should_hide_the_legend() {
let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)]; let data = [(0.0, 5.0), (1.0, 6.0), (3.0, 7.0)];
let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let cases = [ let cases = [
LegendTestCase { LegendTestCase {
chart_area: Rect::new(0, 0, 100, 100), chart_area: Rect::new(0, 0, 100, 100),
@ -562,11 +535,16 @@ mod tests {
}, },
]; ];
for case in &cases { for case in &cases {
let chart: Chart<String, String> = Chart::default() let datasets = (0..10)
.map(|i| {
let name = format!("Dataset #{}", i);
Dataset::default().name(name).data(&data)
})
.collect::<Vec<_>>();
let chart = Chart::new(datasets)
.x_axis(Axis::default().title("X axis")) .x_axis(Axis::default().title("X axis"))
.y_axis(Axis::default().title("Y axis")) .y_axis(Axis::default().title("Y axis"))
.hidden_legend_constraints(case.hidden_legend_constraints) .hidden_legend_constraints(case.hidden_legend_constraints);
.datasets(datasets.as_slice());
let layout = chart.layout(case.chart_area); let layout = chart.layout(case.chart_area);
assert_eq!(layout.legend_area, case.legend_area); assert_eq!(layout.legend_area, case.legend_area);
} }

@ -1,9 +1,10 @@
use unicode_width::UnicodeWidthStr; use crate::{
buffer::Buffer,
use crate::buffer::Buffer; layout::Rect,
use crate::layout::Rect; style::{Color, Style},
use crate::style::{Color, Style}; text::Span,
use crate::widgets::{Block, Widget}; widgets::{Block, Widget},
};
/// A widget to display a task progress. /// A widget to display a task progress.
/// ///
@ -21,7 +22,7 @@ use crate::widgets::{Block, Widget};
pub struct Gauge<'a> { pub struct Gauge<'a> {
block: Option<Block<'a>>, block: Option<Block<'a>>,
ratio: f64, ratio: f64,
label: Option<&'a str>, label: Option<Span<'a>>,
style: Style, style: Style,
} }
@ -61,8 +62,11 @@ impl<'a> Gauge<'a> {
self self
} }
pub fn label(mut self, string: &'a str) -> Gauge<'a> { pub fn label<T>(mut self, label: T) -> Gauge<'a>
self.label = Some(string); where
T: Into<Span<'a>>,
{
self.label = Some(label.into());
self self
} }
@ -74,10 +78,11 @@ impl<'a> Gauge<'a> {
impl<'a> Widget for Gauge<'a> { impl<'a> Widget for Gauge<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(mut self, area: Rect, buf: &mut Buffer) {
let gauge_area = match self.block { let gauge_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };
@ -92,6 +97,11 @@ impl<'a> Widget for Gauge<'a> {
let center = gauge_area.height / 2 + gauge_area.top(); let center = gauge_area.height / 2 + gauge_area.top();
let width = (f64::from(gauge_area.width) * self.ratio).round() as u16; let width = (f64::from(gauge_area.width) * self.ratio).round() as u16;
let end = gauge_area.left() + width; let end = gauge_area.left() + width;
// Label
let ratio = self.ratio;
let label = self
.label
.unwrap_or_else(|| Span::from(format!("{}%", (ratio * 100.0).round())));
for y in gauge_area.top()..gauge_area.bottom() { for y in gauge_area.top()..gauge_area.bottom() {
// Gauge // Gauge
for x in gauge_area.left()..end { for x in gauge_area.left()..end {
@ -99,12 +109,9 @@ impl<'a> Widget for Gauge<'a> {
} }
if y == center { if y == center {
// Label
let precent_label = format!("{}%", (self.ratio * 100.0).round());
let label = self.label.unwrap_or(&precent_label);
let label_width = label.width() as u16; let label_width = label.width() as u16;
let middle = (gauge_area.width - label_width) / 2 + gauge_area.left(); let middle = (gauge_area.width - label_width) / 2 + gauge_area.left();
buf.set_string(middle, y, label, self.style); buf.set_span(middle, y, &label, gauge_area.right() - middle, self.style);
} }
// Fix colors // Fix colors

@ -1,12 +1,13 @@
use crate::{
buffer::Buffer,
layout::{Corner, Rect},
style::{Style, StyleDiff},
text::Text,
widgets::{Block, StatefulWidget, Widget},
};
use std::iter::{self, Iterator}; use std::iter::{self, Iterator};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::{Corner, Rect};
use crate::style::Style;
use crate::widgets::{Block, StatefulWidget, Text, Widget};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ListState { pub struct ListState {
offset: usize, offset: usize,
@ -35,112 +36,110 @@ impl ListState {
} }
} }
#[derive(Debug, Clone)]
pub struct ListItem<'a> {
content: Text<'a>,
style_diff: StyleDiff,
}
impl<'a> ListItem<'a> {
pub fn new<T>(content: T) -> ListItem<'a>
where
T: Into<Text<'a>>,
{
ListItem {
content: content.into(),
style_diff: StyleDiff::default(),
}
}
pub fn style_diff(mut self, style_diff: StyleDiff) -> ListItem<'a> {
self.style_diff = style_diff;
self
}
pub fn height(&self) -> usize {
self.content.height()
}
}
/// A widget to display several items among which one can be selected (optional) /// A widget to display several items among which one can be selected (optional)
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// # use tui::widgets::{Block, Borders, List, Text}; /// # use tui::widgets::{Block, Borders, List, ListItem};
/// # use tui::style::{Style, Color, Modifier}; /// # use tui::style::{Style, StyleDiff, Color, Modifier};
/// let items = ["Item 1", "Item 2", "Item 3"].iter().map(|i| Text::raw(*i)); /// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
/// List::new(items) /// List::new(items)
/// .block(Block::default().title("List").borders(Borders::ALL)) /// .block(Block::default().title("List").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White)) /// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().modifier(Modifier::ITALIC)) /// .highlight_style_diff(StyleDiff::default().modifier(Modifier::ITALIC))
/// .highlight_symbol(">>"); /// .highlight_symbol(">>");
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct List<'b, L> pub struct List<'a> {
where block: Option<Block<'a>>,
L: Iterator<Item = Text<'b>>, items: Vec<ListItem<'a>>,
{ /// Style used as a base style for the widget
block: Option<Block<'b>>,
items: L,
start_corner: Corner,
/// Base style of the widget
style: Style, style: Style,
start_corner: Corner,
/// Style used to render selected item /// Style used to render selected item
highlight_style: Style, highlight_style_diff: StyleDiff,
/// Symbol in front of the selected item (Shift all items to the right) /// 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, L> Default for List<'b, L> impl<'a> List<'a> {
where pub fn new<T>(items: T) -> List<'a>
L: Iterator<Item = Text<'b>> + Default, where
{ T: Into<Vec<ListItem<'a>>>,
fn default() -> List<'b, L> { {
List {
block: None,
items: L::default(),
style: Default::default(),
start_corner: Corner::TopLeft,
highlight_style: Style::default(),
highlight_symbol: None,
}
}
}
impl<'b, L> List<'b, L>
where
L: Iterator<Item = Text<'b>>,
{
pub fn new(items: L) -> List<'b, L> {
List { List {
block: None, block: None,
items, style: Style::default(),
style: Default::default(), items: items.into(),
start_corner: Corner::TopLeft, start_corner: Corner::TopLeft,
highlight_style: Style::default(), highlight_style_diff: StyleDiff::default(),
highlight_symbol: None, highlight_symbol: None,
} }
} }
pub fn block(mut self, block: Block<'b>) -> List<'b, L> { pub fn block(mut self, block: Block<'a>) -> List<'a> {
self.block = Some(block); self.block = Some(block);
self self
} }
pub fn items<I>(mut self, items: I) -> List<'b, L> pub fn style(mut self, style: Style) -> List<'a> {
where
I: IntoIterator<Item = Text<'b>, IntoIter = L>,
{
self.items = items.into_iter();
self
}
pub fn style(mut self, style: Style) -> List<'b, L> {
self.style = style; self.style = style;
self self
} }
pub fn highlight_symbol(mut self, highlight_symbol: &'b str) -> List<'b, L> { pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
self.highlight_symbol = Some(highlight_symbol); self.highlight_symbol = Some(highlight_symbol);
self self
} }
pub fn highlight_style(mut self, highlight_style: Style) -> List<'b, L> { pub fn highlight_style_diff(mut self, diff: StyleDiff) -> List<'a> {
self.highlight_style = highlight_style; self.highlight_style_diff = diff;
self self
} }
pub fn start_corner(mut self, corner: Corner) -> List<'b, L> { pub fn start_corner(mut self, corner: Corner) -> List<'a> {
self.start_corner = corner; self.start_corner = corner;
self self
} }
} }
impl<'b, L> StatefulWidget for List<'b, L> impl<'a> StatefulWidget for List<'a> {
where
L: Iterator<Item = Text<'b>>,
{
type State = ListState; type State = ListState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let list_area = match self.block { let list_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };
@ -149,81 +148,113 @@ where
return; return;
} }
buf.set_background(list_area, self.style.bg);
if self.items.is_empty() {
return;
}
let list_height = list_area.height as usize; let list_height = list_area.height as usize;
buf.set_background(list_area, self.style.bg); let mut start = state.offset;
let mut end = state.offset;
let mut height = 0;
for item in self.items.iter().skip(state.offset) {
if height + item.height() > list_height {
break;
}
height += item.height();
end += 1;
}
let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
while selected >= end {
height = height.saturating_add(self.items[end].height());
end += 1;
while height > list_height {
height = height.saturating_sub(self.items[start].height());
start += 1;
}
}
while selected < start {
start -= 1;
height = height.saturating_add(self.items[start].height());
while height > list_height {
end -= 1;
height = height.saturating_sub(self.items[end].height());
}
}
state.offset = start;
// Use highlight_style only if something is selected
let (selected, highlight_style) = match state.selected {
Some(i) => (Some(i), self.highlight_style),
None => (None, self.style),
};
let highlight_symbol = self.highlight_symbol.unwrap_or(""); let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ") let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width()) .take(highlight_symbol.width())
.collect::<String>(); .collect::<String>();
// Make sure the list show the selected item let mut current_height = 0;
state.offset = if let Some(selected) = selected {
if selected >= list_height + state.offset - 1 {
selected + 1 - list_height
} else if selected < state.offset {
selected
} else {
state.offset
}
} else {
0
};
for (i, item) in self for (i, item) in self
.items .items
.skip(state.offset) .iter_mut()
.enumerate() .enumerate()
.take(list_area.height as usize) .skip(state.offset)
.take(end - start)
{ {
let (x, y) = match self.start_corner { let (x, y) = match self.start_corner {
Corner::TopLeft => (list_area.left(), list_area.top() + i as u16), Corner::BottomLeft => {
Corner::BottomLeft => (list_area.left(), list_area.bottom() - (i + 1) as u16), current_height += item.height() as u16;
// Not supported (list_area.left(), list_area.bottom() - current_height)
_ => (list_area.left(), list_area.top() + i as u16), }
_ => {
let pos = (list_area.left(), list_area.top() + current_height);
current_height += item.height() as u16;
pos
}
};
let area = Rect {
x,
y,
width: list_area.width,
height: item.height() as u16,
}; };
let (elem_x, style) = if let Some(s) = selected { let item_style = self.style.patch(item.style_diff);
if s == i + state.offset { buf.set_background(area, item_style.bg);
let elem_x = if let Some(s) = state.selected {
if s == i {
for line in &mut item.content.lines {
for span in &mut line.0 {
span.style_diff = span.style_diff.patch(self.highlight_style_diff);
}
}
let (x, _) = buf.set_stringn( let (x, _) = buf.set_stringn(
x, x,
y, y,
highlight_symbol, highlight_symbol,
list_area.width as usize, list_area.width as usize,
highlight_style, item_style.patch(self.highlight_style_diff),
); );
(x, Some(highlight_style)) x
} else { } else {
let (x, _) = let (x, _) =
buf.set_stringn(x, y, &blank_symbol, list_area.width as usize, self.style); buf.set_stringn(x, y, &blank_symbol, list_area.width as usize, item_style);
(x, None) x
} }
} else { } else {
(x, None) x
}; };
let max_element_width = (list_area.width - (elem_x - x)) as usize; let max_element_width = (list_area.width - (elem_x - x)) as usize;
match item { for (j, line) in item.content.lines.iter().enumerate() {
Text::Raw(ref v) => { buf.set_spans(
buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(self.style)); elem_x,
} y + j as u16,
Text::Styled(ref v, s) => { line,
buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(s)); max_element_width as u16,
} self.style,
}; );
}
} }
} }
} }
impl<'b, L> Widget for List<'b, L> impl<'a> Widget for List<'a> {
where
L: Iterator<Item = Text<'b>>,
{
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default(); let mut state = ListState::default();
StatefulWidget::render(self, area, buf, &mut state); StatefulWidget::render(self, area, buf, &mut state);

@ -15,9 +15,6 @@
//! - [`Sparkline`] //! - [`Sparkline`]
//! - [`Clear`] //! - [`Clear`]
use bitflags::bitflags;
use std::borrow::Cow;
mod barchart; mod barchart;
mod block; mod block;
pub mod canvas; pub mod canvas;
@ -36,15 +33,14 @@ pub use self::block::{Block, BorderType};
pub use self::chart::{Axis, Chart, Dataset, GraphType}; pub use self::chart::{Axis, Chart, Dataset, GraphType};
pub use self::clear::Clear; pub use self::clear::Clear;
pub use self::gauge::Gauge; pub use self::gauge::Gauge;
pub use self::list::{List, ListState}; pub use self::list::{List, ListItem, ListState};
pub use self::paragraph::{Paragraph, Wrap}; pub use self::paragraph::{Paragraph, Wrap};
pub use self::sparkline::Sparkline; pub use self::sparkline::Sparkline;
pub use self::table::{Row, Table, TableState}; pub use self::table::{Row, Table, TableState};
pub use self::tabs::Tabs; pub use self::tabs::Tabs;
use crate::buffer::Buffer; use crate::{buffer::Buffer, layout::Rect};
use crate::layout::Rect; use bitflags::bitflags;
use crate::style::Style;
bitflags! { bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget. /// Bitflags that can be composed to set the visible borders essentially on the block widget.
@ -64,22 +60,6 @@ bitflags! {
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum Text<'b> {
Raw(Cow<'b, str>),
Styled(Cow<'b, str>, Style),
}
impl<'b> Text<'b> {
pub fn raw<D: Into<Cow<'b, str>>>(data: D) -> Text<'b> {
Text::Raw(data.into())
}
pub fn styled<D: Into<Cow<'b, str>>>(data: D, style: Style) -> Text<'b> {
Text::Styled(data.into(), style)
}
}
/// Base requirements for a Widget /// Base requirements for a Widget
pub trait Widget { pub trait Widget {
/// Draws the current state of the widget in the given buffer. That the only method required to /// Draws the current state of the widget in the given buffer. That the only method required to
@ -108,7 +88,7 @@ pub trait Widget {
/// # use std::io; /// # use std::io;
/// # use tui::Terminal; /// # use tui::Terminal;
/// # use tui::backend::{Backend, TermionBackend}; /// # use tui::backend::{Backend, TermionBackend};
/// # use tui::widgets::{Widget, List, ListState, Text}; /// # use tui::widgets::{Widget, List, ListItem, ListState};
/// ///
/// // Let's say we have some events to display. /// // Let's say we have some events to display.
/// struct Events { /// struct Events {
@ -187,7 +167,7 @@ pub trait Widget {
/// terminal.draw(|f| { /// terminal.draw(|f| {
/// // The items managed by the application are transformed to something /// // The items managed by the application are transformed to something
/// // that is understood by tui. /// // that is understood by tui.
/// let items = events.items.iter().map(Text::raw); /// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
/// // The `List` widget is then built with those items. /// // The `List` widget is then built with those items.
/// let list = List::new(items); /// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is /// // Finally the widget is rendered using the associated state. `events.state` is

@ -1,13 +1,16 @@
use either::Either; use crate::{
use unicode_segmentation::UnicodeSegmentation; buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
text::{StyledGrapheme, Text},
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper},
Block, Widget,
},
};
use std::iter;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::buffer::Buffer;
use crate::layout::{Alignment, Rect};
use crate::style::Style;
use crate::widgets::reflow::{LineComposer, LineTruncator, Styled, WordWrapper};
use crate::widgets::{Block, Text, Widget};
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
match alignment { match alignment {
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2), Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
@ -21,24 +24,26 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # Examples /// # Examples
/// ///
/// ``` /// ```
/// # use tui::widgets::{Block, Borders, Paragraph, Text, Wrap}; /// # use tui::text::{Text, Spans, Span};
/// # use tui::style::{Style, Color}; /// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use tui::style::{Style, StyleDiff, Color, Modifier};
/// # use tui::layout::{Alignment}; /// # use tui::layout::{Alignment};
/// let text = [ /// let text = vec![
/// Text::raw("First line\n"), /// Spans::from(vec![
/// Text::styled("Second line\n", Style::default().fg(Color::Red)) /// Span::raw("First"),
/// Span::styled("line",StyleDiff::default().add_modifier(Modifier::ITALIC)),
/// Span::raw("."),
/// ]),
/// Spans::from(Span::styled("Second line", StyleDiff::default().fg(Color::Red))),
/// ]; /// ];
/// Paragraph::new(text.iter()) /// Paragraph::new(text)
/// .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))
/// .alignment(Alignment::Center) /// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true }); /// .wrap(Wrap { trim: true });
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Paragraph<'a, 't, T> pub struct Paragraph<'a> {
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
@ -46,9 +51,7 @@ where
/// How to wrap the text /// How to wrap the text
wrap: Option<Wrap>, wrap: Option<Wrap>,
/// The text to display /// The text to display
text: T, text: Text<'a>,
/// Should we parse the text for embedded commands
raw: bool,
/// Scroll /// Scroll
scroll: (u16, u16), scroll: (u16, u16),
/// Alignment of the text /// Alignment of the text
@ -57,16 +60,17 @@ where
/// Describes how to wrap text across lines. /// Describes how to wrap text across lines.
/// ///
/// # Example /// ## Examples
/// ///
/// ``` /// ```
/// # use tui::widgets::{Paragraph, Text, Wrap}; /// # use tui::widgets::{Paragraph, Wrap};
/// let bullet_points = [Text::raw(r#"Some indented points: /// # use tui::text::Text;
/// let bullet_points = Text::from(r#"Some indented points:
/// - First thing goes here and is long so that it wraps /// - First thing goes here and is long so that it wraps
/// - Here is another point that is long enough to wrap"#)]; /// - Here is another point that is long enough to wrap"#);
/// ///
/// // With leading spaces trimmed (window width of 30 chars): /// // With leading spaces trimmed (window width of 30 chars):
/// Paragraph::new(bullet_points.iter()).wrap(Wrap { trim: true }); /// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
/// // Some indented points: /// // Some indented points:
/// // - First thing goes here and is /// // - First thing goes here and is
/// // long so that it wraps /// // long so that it wraps
@ -74,74 +78,67 @@ where
/// // is long enough to wrap /// // is long enough to wrap
/// ///
/// // But without trimming, indentation is preserved: /// // But without trimming, indentation is preserved:
/// Paragraph::new(bullet_points.iter()).wrap(Wrap { trim: false }); /// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
/// // Some indented points: /// // Some indented points:
/// // - First thing goes here /// // - First thing goes here
/// // and is long so that it wraps /// // and is long so that it wraps
/// // - Here is another point /// // - Here is another point
/// // that is long enough to wrap /// // that is long enough to wrap
/// ```
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Wrap { pub struct Wrap {
/// Should leading whitespace be trimmed /// Should leading whitespace be trimmed
pub trim: bool, pub trim: bool,
} }
impl<'a, 't, T> Paragraph<'a, 't, T> impl<'a> Paragraph<'a> {
where pub fn new<T>(text: T) -> Paragraph<'a>
T: Iterator<Item = &'t Text<'t>>, where
{ T: Into<Text<'a>>,
pub fn new(text: T) -> Paragraph<'a, 't, T> { {
Paragraph { Paragraph {
block: None, block: None,
style: Default::default(), style: Default::default(),
wrap: None, wrap: None,
raw: false, text: text.into(),
text,
scroll: (0, 0), scroll: (0, 0),
alignment: Alignment::Left, alignment: Alignment::Left,
} }
} }
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a, 't, T> { pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
self.block = Some(block); self.block = Some(block);
self self
} }
pub fn style(mut self, style: Style) -> Paragraph<'a, 't, T> { pub fn style(mut self, style: Style) -> Paragraph<'a> {
self.style = style; self.style = style;
self self
} }
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a, 't, T> { pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
self.wrap = Some(wrap); self.wrap = Some(wrap);
self self
} }
pub fn raw(mut self, flag: bool) -> Paragraph<'a, 't, T> { pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
self.raw = flag;
self
}
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a, 't, T> {
self.scroll = offset; self.scroll = offset;
self self
} }
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a, 't, T> { pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
self.alignment = alignment; self.alignment = alignment;
self self
} }
} }
impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T> impl<'a> Widget for Paragraph<'a> {
where
T: Iterator<Item = &'t Text<'t>>,
{
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(mut self, area: Rect, buf: &mut Buffer) {
let text_area = match self.block { let text_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };
@ -153,15 +150,17 @@ where
buf.set_background(text_area, self.style.bg); buf.set_background(text_area, self.style.bg);
let style = self.style; let style = self.style;
let mut styled = self.text.by_ref().flat_map(|t| match *t { let mut styled = self.text.lines.iter().flat_map(|spans| {
Text::Raw(ref d) => { spans
let data: &'t str = d; // coerce to &str .0
Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| Styled(g, style))) .iter()
} .flat_map(|span| span.styled_graphemes(style))
Text::Styled(ref d, s) => { // Required given the way composers work but might be refactored out if we change
let data: &'t str = d; // coerce to &str // composers to operate on lines instead of a stream of graphemes.
Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s))) .chain(iter::once(StyledGrapheme {
} symbol: "\n",
style: self.style,
}))
}); });
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap { let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
@ -177,7 +176,7 @@ where
while let Some((current_line, current_line_width)) = line_composer.next_line() { while let Some((current_line, current_line_width)) = line_composer.next_line() {
if y >= self.scroll.0 { if y >= self.scroll.0 {
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment); let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for Styled(symbol, style) in current_line { for StyledGrapheme { symbol, style } in current_line {
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0) buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
.set_symbol(if symbol.is_empty() { .set_symbol(if symbol.is_empty() {
// If the symbol is empty, the last char which rendered last time will // If the symbol is empty, the last char which rendered last time will

@ -1,32 +1,29 @@
use crate::style::Style; use crate::text::StyledGrapheme;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
const NBSP: &str = "\u{00a0}"; const NBSP: &str = "\u{00a0}";
#[derive(Copy, Clone, Debug)]
pub struct Styled<'a>(pub &'a str, pub Style);
/// A state machine to pack styled symbols into lines. /// A state machine to pack styled symbols into lines.
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming /// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
/// iterators for that). /// iterators for that).
pub trait LineComposer<'a> { pub trait LineComposer<'a> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)>; fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
} }
/// A state machine that wraps lines on word boundaries. /// A state machine that wraps lines on word boundaries.
pub struct WordWrapper<'a, 'b> { pub struct WordWrapper<'a, 'b> {
symbols: &'b mut dyn Iterator<Item = Styled<'a>>, symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16, max_line_width: u16,
current_line: Vec<Styled<'a>>, current_line: Vec<StyledGrapheme<'a>>,
next_line: Vec<Styled<'a>>, next_line: Vec<StyledGrapheme<'a>>,
/// Removes the leading whitespace from lines /// Removes the leading whitespace from lines
trim: bool, trim: bool,
} }
impl<'a, 'b> WordWrapper<'a, 'b> { impl<'a, 'b> WordWrapper<'a, 'b> {
pub fn new( pub fn new(
symbols: &'b mut dyn Iterator<Item = Styled<'a>>, symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16, max_line_width: u16,
trim: bool, trim: bool,
) -> WordWrapper<'a, 'b> { ) -> WordWrapper<'a, 'b> {
@ -41,7 +38,7 @@ impl<'a, 'b> WordWrapper<'a, 'b> {
} }
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> { impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> { fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
if self.max_line_width == 0 { if self.max_line_width == 0 {
return None; return None;
} }
@ -51,14 +48,14 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
let mut current_line_width = self let mut current_line_width = self
.current_line .current_line
.iter() .iter()
.map(|Styled(c, _)| c.width() as u16) .map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
.sum(); .sum();
let mut symbols_to_last_word_end: usize = 0; let mut symbols_to_last_word_end: usize = 0;
let mut width_to_last_word_end: u16 = 0; let mut width_to_last_word_end: u16 = 0;
let mut prev_whitespace = false; let mut prev_whitespace = false;
let mut symbols_exhausted = true; let mut symbols_exhausted = true;
for Styled(symbol, style) in &mut self.symbols { for StyledGrapheme { symbol, style } in &mut self.symbols {
symbols_exhausted = false; symbols_exhausted = false;
let symbol_whitespace = symbol.chars().all(&char::is_whitespace); let symbol_whitespace = symbol.chars().all(&char::is_whitespace);
@ -85,7 +82,7 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
width_to_last_word_end = current_line_width; width_to_last_word_end = current_line_width;
} }
self.current_line.push(Styled(symbol, style)); self.current_line.push(StyledGrapheme { symbol, style });
current_line_width += symbol.width() as u16; current_line_width += symbol.width() as u16;
if current_line_width > self.max_line_width { if current_line_width > self.max_line_width {
@ -99,9 +96,10 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
// Push the remainder to the next line but strip leading whitespace: // Push the remainder to the next line but strip leading whitespace:
{ {
let remainder = &self.current_line[truncate_at..]; let remainder = &self.current_line[truncate_at..];
if let Some(remainder_nonwhite) = remainder if let Some(remainder_nonwhite) =
.iter() remainder.iter().position(|StyledGrapheme { symbol, .. }| {
.position(|Styled(c, _)| !c.chars().all(&char::is_whitespace)) !symbol.chars().all(&char::is_whitespace)
})
{ {
self.next_line self.next_line
.extend_from_slice(&remainder[remainder_nonwhite..]); .extend_from_slice(&remainder[remainder_nonwhite..]);
@ -126,16 +124,16 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
/// A state machine that truncates overhanging lines. /// A state machine that truncates overhanging lines.
pub struct LineTruncator<'a, 'b> { pub struct LineTruncator<'a, 'b> {
symbols: &'b mut dyn Iterator<Item = Styled<'a>>, symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16, max_line_width: u16,
current_line: Vec<Styled<'a>>, current_line: Vec<StyledGrapheme<'a>>,
/// Record the offet to skip render /// Record the offet to skip render
horizontal_offset: u16, horizontal_offset: u16,
} }
impl<'a, 'b> LineTruncator<'a, 'b> { impl<'a, 'b> LineTruncator<'a, 'b> {
pub fn new( pub fn new(
symbols: &'b mut dyn Iterator<Item = Styled<'a>>, symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16, max_line_width: u16,
) -> LineTruncator<'a, 'b> { ) -> LineTruncator<'a, 'b> {
LineTruncator { LineTruncator {
@ -152,7 +150,7 @@ impl<'a, 'b> LineTruncator<'a, 'b> {
} }
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> { impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
fn next_line(&mut self) -> Option<(&[Styled<'a>], u16)> { fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
if self.max_line_width == 0 { if self.max_line_width == 0 {
return None; return None;
} }
@ -163,7 +161,7 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
let mut skip_rest = false; let mut skip_rest = false;
let mut symbols_exhausted = true; let mut symbols_exhausted = true;
let mut horizontal_offset = self.horizontal_offset as usize; let mut horizontal_offset = self.horizontal_offset as usize;
for Styled(symbol, style) in &mut self.symbols { for StyledGrapheme { symbol, style } in &mut self.symbols {
symbols_exhausted = false; symbols_exhausted = false;
// Ignore characters wider that the total max width. // Ignore characters wider that the total max width.
@ -196,11 +194,11 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
} }
}; };
current_line_width += symbol.width() as u16; current_line_width += symbol.width() as u16;
self.current_line.push(Styled(symbol, style)); self.current_line.push(StyledGrapheme { symbol, style });
} }
if skip_rest { if skip_rest {
for Styled(symbol, _) in &mut self.symbols { for StyledGrapheme { symbol, .. } in &mut self.symbols {
if symbol == "\n" { if symbol == "\n" {
break; break;
} }
@ -243,7 +241,8 @@ mod test {
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) { fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
let style = Default::default(); let style = Default::default();
let mut styled = UnicodeSegmentation::graphemes(text, true).map(|g| Styled(g, style)); let mut styled =
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
let mut composer: Box<dyn LineComposer> = match which { let mut composer: Box<dyn LineComposer> = match which {
Composer::WordWrapper { trim } => { Composer::WordWrapper { trim } => {
Box::new(WordWrapper::new(&mut styled, text_area_width, trim)) Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
@ -255,7 +254,7 @@ mod test {
while let Some((styled, width)) = composer.next_line() { while let Some((styled, width)) = composer.next_line() {
let line = styled let line = styled
.iter() .iter()
.map(|Styled(g, _style)| *g) .map(|StyledGrapheme { symbol, .. }| *symbol)
.collect::<String>(); .collect::<String>();
assert!(width <= text_area_width); assert!(width <= text_area_width);
lines.push(line); lines.push(line);

@ -76,10 +76,11 @@ impl<'a> Sparkline<'a> {
impl<'a> Widget for Sparkline<'a> { impl<'a> Widget for Sparkline<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(mut self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block { let spark_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };

@ -221,10 +221,11 @@ where
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// Render block if necessary and get the drawing area // Render block if necessary and get the drawing area
let table_area = match self.block { let table_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };

@ -1,10 +1,11 @@
use unicode_width::UnicodeWidthStr; use crate::{
buffer::Buffer,
use crate::buffer::Buffer; layout::Rect,
use crate::layout::Rect; style::{Style, StyleDiff},
use crate::style::Style; symbols,
use crate::symbols::line; text::{Span, Spans},
use crate::widgets::{Block, Widget}; widgets::{Block, Widget},
};
/// A widget to display available tabs in a multiple panels context. /// A widget to display available tabs in a multiple panels context.
/// ///
@ -13,93 +14,85 @@ use crate::widgets::{Block, Widget};
/// ``` /// ```
/// # use tui::widgets::{Block, Borders, Tabs}; /// # use tui::widgets::{Block, Borders, Tabs};
/// # use tui::style::{Style, Color}; /// # use tui::style::{Style, Color};
/// # use tui::text::{Spans};
/// # use tui::symbols::{DOT}; /// # use tui::symbols::{DOT};
/// Tabs::default() /// let titles = ["Tab1", "Tab2", "Tab3", "Tab4"].iter().cloned().map(Spans::from).collect();
/// Tabs::new(titles)
/// .block(Block::default().title("Tabs").borders(Borders::ALL)) /// .block(Block::default().title("Tabs").borders(Borders::ALL))
/// .titles(&["Tab1", "Tab2", "Tab3", "Tab4"])
/// .style(Style::default().fg(Color::White)) /// .style(Style::default().fg(Color::White))
/// .highlight_style(Style::default().fg(Color::Yellow)) /// .highlight_style(Style::default().fg(Color::Yellow))
/// .divider(DOT); /// .divider(DOT);
/// ``` /// ```
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Tabs<'a, T> pub struct Tabs<'a> {
where
T: AsRef<str> + 'a,
{
/// A block to wrap this widget in if necessary /// A block to wrap this widget in if necessary
block: Option<Block<'a>>, block: Option<Block<'a>>,
/// One title for each tab /// One title for each tab
titles: &'a [T], titles: Vec<Spans<'a>>,
/// The index of the selected tabs /// The index of the selected tabs
selected: usize, selected: usize,
/// The style used to draw the text /// The style used to draw the text
style: Style, style: Style,
/// The style used to display the selected item /// Style diff to apply to the selected item
highlight_style: Style, highlight_style_diff: StyleDiff,
/// Tab divider /// Tab divider
divider: &'a str, divider: Span<'a>,
} }
impl<'a, T> Default for Tabs<'a, T> impl<'a> Tabs<'a> {
where pub fn new(titles: Vec<Spans<'a>>) -> Tabs<'a> {
T: AsRef<str>,
{
fn default() -> Tabs<'a, T> {
Tabs { Tabs {
block: None, block: None,
titles: &[], titles,
selected: 0, selected: 0,
style: Default::default(), style: Default::default(),
highlight_style: Default::default(), highlight_style_diff: Default::default(),
divider: line::VERTICAL, divider: Span::raw(symbols::line::VERTICAL),
} }
} }
}
impl<'a, T> Tabs<'a, T> pub fn block(mut self, block: Block<'a>) -> Tabs<'a> {
where
T: AsRef<str>,
{
pub fn block(mut self, block: Block<'a>) -> Tabs<'a, T> {
self.block = Some(block); self.block = Some(block);
self self
} }
pub fn titles(mut self, titles: &'a [T]) -> Tabs<'a, T> { pub fn select(mut self, selected: usize) -> Tabs<'a> {
self.titles = titles; self.selected = selected;
self self
} }
pub fn select(mut self, selected: usize) -> Tabs<'a, T> { pub fn style(mut self, style: Style) -> Tabs<'a> {
self.selected = selected; self.style = style;
self self
} }
pub fn style(mut self, style: Style) -> Tabs<'a, T> { #[deprecated(since = "0.10.0", note = "You should use `Tabs::highlight_style_diff`")]
self.style = style; pub fn highlight_style(mut self, style: Style) -> Tabs<'a> {
self.highlight_style_diff = StyleDiff::from(style);
self self
} }
pub fn highlight_style(mut self, style: Style) -> Tabs<'a, T> { pub fn highlight_style_diff(mut self, diff: StyleDiff) -> Tabs<'a> {
self.highlight_style = style; self.highlight_style_diff = diff;
self self
} }
pub fn divider(mut self, divider: &'a str) -> Tabs<'a, T> { pub fn divider<T>(mut self, divider: T) -> Tabs<'a>
self.divider = divider; where
T: Into<Span<'a>>,
{
self.divider = divider.into();
self self
} }
} }
impl<'a, T> Widget for Tabs<'a, T> impl<'a> Widget for Tabs<'a> {
where
T: AsRef<str>,
{
fn render(mut self, area: Rect, buf: &mut Buffer) { fn render(mut self, area: Rect, buf: &mut Buffer) {
let tabs_area = match self.block { let tabs_area = match self.block.take() {
Some(ref mut b) => { Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf); b.render(area, buf);
b.inner(area) inner_area
} }
None => area, None => area,
}; };
@ -112,28 +105,32 @@ where
let mut x = tabs_area.left(); let mut x = tabs_area.left();
let titles_length = self.titles.len(); let titles_length = self.titles.len();
let divider_width = self.divider.width() as u16; for (i, mut title) in self.titles.into_iter().enumerate() {
for (title, style, last_title) in self.titles.iter().enumerate().map(|(i, t)| { let last_title = titles_length - 1 == i;
let lt = i + 1 == titles_length;
if i == self.selected { if i == self.selected {
(t, self.highlight_style, lt) for span in &mut title.0 {
} else { span.style_diff = span.style_diff.patch(self.highlight_style_diff);
(t, self.style, lt) }
} }
}) { x = x.saturating_add(1);
x += 1; let remaining_width = tabs_area.right().saturating_sub(x);
if x >= tabs_area.right() { if remaining_width == 0 {
break;
}
let pos = buf.set_spans(x, tabs_area.top(), &title, remaining_width, self.style);
x = pos.0.saturating_add(1);
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 || last_title {
break; break;
} else {
buf.set_string(x, tabs_area.top(), title.as_ref(), style);
x += title.as_ref().width() as u16 + 1;
if x >= tabs_area.right() || last_title {
break;
} else {
buf.set_string(x, tabs_area.top(), self.divider, self.style);
x += divider_width;
}
} }
let pos = buf.set_span(
x,
tabs_area.top(),
&self.divider,
remaining_width,
self.style,
);
x = pos.0;
} }
} }
} }

@ -1,9 +1,12 @@
use tui::backend::TestBackend; use tui::{
use tui::buffer::Buffer; backend::TestBackend,
use tui::layout::Rect; buffer::Buffer,
use tui::style::{Color, Style}; layout::Rect,
use tui::widgets::{Block, Borders}; style::{Color, StyleDiff},
use tui::Terminal; text::Span,
widgets::{Block, Borders},
Terminal,
};
#[test] #[test]
fn widgets_block_renders() { fn widgets_block_renders() {
@ -12,9 +15,11 @@ fn widgets_block_renders() {
terminal terminal
.draw(|f| { .draw(|f| {
let block = Block::default() let block = Block::default()
.title("Title") .title(Span::styled(
.borders(Borders::ALL) "Title",
.title_style(Style::default().fg(Color::LightBlue)); StyleDiff::default().fg(Color::LightBlue),
))
.borders(Borders::ALL);
f.render_widget( f.render_widget(
block, block,
Rect { Rect {

@ -3,10 +3,15 @@ use tui::{
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, Style},
symbols, symbols,
text::Span,
widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line}, widgets::{Axis, Block, Borders, Chart, Dataset, GraphType::Line},
Terminal, Terminal,
}; };
fn create_labels<'a>(labels: &'a [&'a str]) -> Vec<Span<'a>> {
labels.iter().map(|l| Span::from(*l)).collect()
}
#[test] #[test]
fn widgets_chart_can_have_axis_with_zero_length_bounds() { fn widgets_chart_can_have_axis_with_zero_length_bounds() {
let backend = TestBackend::new(100, 100); let backend = TestBackend::new(100, 100);
@ -14,15 +19,22 @@ fn widgets_chart_can_have_axis_with_zero_length_bounds() {
terminal terminal
.draw(|f| { .draw(|f| {
let datasets = [Dataset::default() let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille) .marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta)) .style(Style::default().fg(Color::Magenta))
.data(&[(0.0, 0.0)])]; .data(&[(0.0, 0.0)])];
let chart = Chart::default() let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL)) .block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis(Axis::default().bounds([0.0, 0.0]).labels(&["0.0", "1.0"])) .x_axis(
.y_axis(Axis::default().bounds([0.0, 0.0]).labels(&["0.0", "1.0"])) Axis::default()
.datasets(&datasets); .bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget( f.render_widget(
chart, chart,
Rect { Rect {
@ -43,7 +55,7 @@ fn widgets_chart_handles_overflows() {
terminal terminal
.draw(|f| { .draw(|f| {
let datasets = [Dataset::default() let datasets = vec![Dataset::default()
.marker(symbols::Marker::Braille) .marker(symbols::Marker::Braille)
.style(Style::default().fg(Color::Magenta)) .style(Style::default().fg(Color::Magenta))
.data(&[ .data(&[
@ -51,15 +63,18 @@ fn widgets_chart_handles_overflows() {
(1_588_298_473.0, 0.0), (1_588_298_473.0, 0.0),
(1_588_298_496.0, 1.0), (1_588_298_496.0, 1.0),
])]; ])];
let chart = Chart::default() let chart = Chart::new(datasets)
.block(Block::default().title("Plot").borders(Borders::ALL)) .block(Block::default().title("Plot").borders(Borders::ALL))
.x_axis( .x_axis(
Axis::default() Axis::default()
.bounds([1_588_298_471.0, 1_588_992_600.0]) .bounds([1_588_298_471.0, 1_588_992_600.0])
.labels(&["1588298471.0", "1588992600.0"]), .labels(create_labels(&["1588298471.0", "1588992600.0"])),
) )
.y_axis(Axis::default().bounds([0.0, 1.0]).labels(&["0.0", "1.0"])) .y_axis(
.datasets(&datasets); Axis::default()
.bounds([0.0, 1.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget( f.render_widget(
chart, chart,
Rect { Rect {
@ -80,16 +95,23 @@ fn widgets_chart_can_have_empty_datasets() {
terminal terminal
.draw(|f| { .draw(|f| {
let datasets = [Dataset::default().data(&[]).graph_type(Line)]; let datasets = vec![Dataset::default().data(&[]).graph_type(Line)];
let chart = Chart::default() let chart = Chart::new(datasets)
.block( .block(
Block::default() Block::default()
.title("Empty Dataset With Line") .title("Empty Dataset With Line")
.borders(Borders::ALL), .borders(Borders::ALL),
) )
.x_axis(Axis::default().bounds([0.0, 0.0]).labels(&["0.0", "1.0"])) .x_axis(
.y_axis(Axis::default().bounds([0.0, 1.0]).labels(&["0.0", "1.0"])) Axis::default()
.datasets(&datasets); .bounds([0.0, 0.0])
.labels(create_labels(&["0.0", "1.0"])),
)
.y_axis(
Axis::default()
.bounds([0.0, 1.0])
.labels(create_labels(&["0.0", "1.0"])),
);
f.render_widget( f.render_widget(
chart, chart,
Rect { Rect {

@ -2,9 +2,9 @@ use tui::{
backend::TestBackend, backend::TestBackend,
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
style::{Color, Style}, style::{Color, StyleDiff},
symbols, symbols,
widgets::{Block, Borders, List, ListState, Text}, widgets::{Block, Borders, List, ListItem, ListState},
Terminal, Terminal,
}; };
@ -18,12 +18,12 @@ fn widgets_list_should_highlight_the_selected_item() {
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let items = vec![ let items = vec![
Text::raw("Item 1"), ListItem::new("Item 1"),
Text::raw("Item 2"), ListItem::new("Item 2"),
Text::raw("Item 3"), ListItem::new("Item 3"),
]; ];
let list = List::new(items.into_iter()) let list = List::new(items)
.highlight_style(Style::default().bg(Color::Yellow)) .highlight_style_diff(StyleDiff::default().bg(Color::Yellow))
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(list, size, &mut state); f.render_stateful_widget(list, size, &mut state);
}) })
@ -42,7 +42,7 @@ fn widgets_list_should_truncate_items() {
struct TruncateTestCase<'a> { struct TruncateTestCase<'a> {
selected: Option<usize>, selected: Option<usize>,
items: Vec<Text<'a>>, items: Vec<ListItem<'a>>,
expected: Buffer, expected: Buffer,
} }
@ -50,7 +50,10 @@ fn widgets_list_should_truncate_items() {
// An item is selected // An item is selected
TruncateTestCase { TruncateTestCase {
selected: Some(0), selected: Some(0),
items: vec![Text::raw("A very long line"), Text::raw("A very long line")], items: vec![
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![ expected: Buffer::with_lines(vec![
format!(">> A ve{} ", symbols::line::VERTICAL), format!(">> A ve{} ", symbols::line::VERTICAL),
format!(" A ve{} ", symbols::line::VERTICAL), format!(" A ve{} ", symbols::line::VERTICAL),
@ -59,20 +62,22 @@ fn widgets_list_should_truncate_items() {
// No item is selected // No item is selected
TruncateTestCase { TruncateTestCase {
selected: None, selected: None,
items: vec![Text::raw("A very long line"), Text::raw("A very long line")], items: vec![
ListItem::new("A very long line"),
ListItem::new("A very long line"),
],
expected: Buffer::with_lines(vec![ expected: Buffer::with_lines(vec![
format!("A very {} ", symbols::line::VERTICAL), format!("A very {} ", symbols::line::VERTICAL),
format!("A very {} ", symbols::line::VERTICAL), format!("A very {} ", symbols::line::VERTICAL),
]), ]),
}, },
]; ];
for mut case in cases { for case in cases {
let mut state = ListState::default(); let mut state = ListState::default();
state.select(case.selected); state.select(case.selected);
let items = case.items.drain(..);
terminal terminal
.draw(|f| { .draw(|f| {
let list = List::new(items) let list = List::new(case.items.clone())
.block(Block::default().borders(Borders::RIGHT)) .block(Block::default().borders(Borders::RIGHT))
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state); f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state);

@ -2,7 +2,8 @@ use tui::{
backend::TestBackend, backend::TestBackend,
buffer::Buffer, buffer::Buffer,
layout::Alignment, layout::Alignment,
widgets::{Block, Borders, Paragraph, Text, Wrap}, text::{Spans, Text},
widgets::{Block, Borders, Paragraph, Wrap},
Terminal, Terminal,
}; };
@ -20,8 +21,8 @@ fn widgets_paragraph_can_wrap_its_content() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let text = [Text::raw(SAMPLE_STRING)]; let text = vec![Spans::from(SAMPLE_STRING)];
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.alignment(alignment) .alignment(alignment)
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
@ -87,8 +88,8 @@ fn widgets_paragraph_renders_double_width_graphemes() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let text = [Text::raw(s)]; let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
f.render_widget(paragraph, size); f.render_widget(paragraph, size);
@ -119,8 +120,8 @@ fn widgets_paragraph_renders_mixed_width_graphemes() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let text = [Text::raw(s)]; let text = vec![Spans::from(s)];
let paragraph = Paragraph::new(text.iter()) let paragraph = Paragraph::new(text)
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.wrap(Wrap { trim: true }); .wrap(Wrap { trim: true });
f.render_widget(paragraph, size); f.render_widget(paragraph, size);
@ -149,13 +150,10 @@ fn widgets_paragraph_can_scroll_horizontally() {
terminal terminal
.draw(|f| { .draw(|f| {
let size = f.size(); let size = f.size();
let text = [Text::raw( let text = Text::from(
" "段落现在可以水平滚动了!\nParagraph can scroll horizontally!\nShort line",
Paragraph can scroll horizontally! );
Short line let paragraph = Paragraph::new(text)
",
)];
let paragraph = Paragraph::new(text.iter())
.block(Block::default().borders(Borders::ALL)) .block(Block::default().borders(Borders::ALL))
.alignment(alignment) .alignment(alignment)
.scroll(scroll); .scroll(scroll);

@ -1,4 +1,7 @@
use tui::{backend::TestBackend, buffer::Buffer, layout::Rect, symbols, widgets::Tabs, Terminal}; use tui::{
backend::TestBackend, buffer::Buffer, layout::Rect, symbols, text::Spans, widgets::Tabs,
Terminal,
};
#[test] #[test]
fn widgets_tabs_should_not_panic_on_narrow_areas() { fn widgets_tabs_should_not_panic_on_narrow_areas() {
@ -6,7 +9,7 @@ fn widgets_tabs_should_not_panic_on_narrow_areas() {
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
terminal terminal
.draw(|f| { .draw(|f| {
let tabs = Tabs::default().titles(&["Tab1", "Tab2"]); let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
f.render_widget( f.render_widget(
tabs, tabs,
Rect { Rect {
@ -28,7 +31,7 @@ fn widgets_tabs_should_truncate_the_last_item() {
let mut terminal = Terminal::new(backend).unwrap(); let mut terminal = Terminal::new(backend).unwrap();
terminal terminal
.draw(|f| { .draw(|f| {
let tabs = Tabs::default().titles(&["Tab1", "Tab2"]); let tabs = Tabs::new(["Tab1", "Tab2"].iter().cloned().map(Spans::from).collect());
f.render_widget( f.render_widget(
tabs, tabs,
Rect { Rect {
@ -40,6 +43,6 @@ fn widgets_tabs_should_truncate_the_last_item() {
); );
}) })
.unwrap(); .unwrap();
let expected = Buffer::with_lines(vec![format!(" Tab1 {} Ta", symbols::line::VERTICAL)]); let expected = Buffer::with_lines(vec![format!(" Tab1 {} T ", symbols::line::VERTICAL)]);
terminal.backend().assert_buffer(&expected); terminal.backend().assert_buffer(&expected);
} }

Loading…
Cancel
Save