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]
bitflags = "1.0"
cassowary = "0.3"
itertools = "0.9"
either = "1.5"
unicode-segmentation = "1.2"
unicode-width = "0.1"
termion = { version = "1.5", optional = true }

@ -6,6 +6,7 @@ SHELL=/bin/bash
RUST_CHANNEL ?= stable
CARGO_FLAGS =
RUSTUP_INSTALLED = $(shell command -v rustup 2> /dev/null)
TEST_FILTER ?=
ifndef RUSTUP_INSTALLED
CARGO = cargo
@ -59,16 +60,16 @@ clippy: ## Check the style of the source code and catch common errors
.PHONY: test
test: ## Run the tests
$(CARGO) test --all-features
$(CARGO) test --all-features $(TEST_FILTER)
# =============================== Examples ====================================
.PHONY: build-examples
build-examples: ## Build all examples
@$(CARGO) build --examples --all-features
@$(CARGO) build --release --examples --all-features
.PHONY: run-examples
run-examples: ## Run all examples
run-examples: build-examples ## Run all examples
@for file in examples/*.rs; do \
name=$$(basename $${file/.rs/}); \
$(CARGO) run --all-features --release --example $$name; \
@ -78,6 +79,7 @@ run-examples: ## Run all examples
.PHONY: doc
doc: RUST_CHANNEL = nightly
doc: ## Build the documentation (available at ./target/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
.PHONY: watch-doc
watch-doc: RUST_CHANNEL = nightly
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 =================================

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

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

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

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

@ -10,27 +10,46 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{
backend::TermionBackend,
layout::{Constraint, Corner, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, Text},
style::{Color, Modifier, Style, StyleDiff},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem},
Terminal,
};
struct App<'a> {
items: StatefulList<&'a str>,
items: StatefulList<(&'a str, usize)>,
events: Vec<(&'a str, &'a str)>,
info_style: Style,
warning_style: Style,
error_style: Style,
critical_style: Style,
}
impl<'a> App<'a> {
fn new() -> App<'a> {
App {
items: StatefulList::with_items(vec![
"Item0", "Item1", "Item2", "Item3", "Item4", "Item5", "Item6", "Item7", "Item8",
"Item9", "Item10", "Item11", "Item12", "Item13", "Item14", "Item15", "Item16",
"Item17", "Item18", "Item19", "Item20", "Item21", "Item22", "Item23", "Item24",
("Item0", 1),
("Item1", 2),
("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![
("Event1", "INFO"),
@ -60,10 +79,6 @@ impl<'a> App<'a> {
("Event25", "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 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)
.block(Block::default().borders(Borders::ALL).title("List"))
.style(style)
.highlight_style(style.fg(Color::LightGreen).modifier(Modifier::BOLD))
.highlight_symbol(">");
.highlight_style_diff(
StyleDiff::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol(">> ");
f.render_stateful_widget(items, chunks[0], &mut app.items.state);
let events = app.events.iter().map(|&(evt, level)| {
Text::styled(
format!("{}: {}", level, evt),
match level {
"ERROR" => app.error_style,
"CRITICAL" => app.critical_style,
"WARNING" => app.warning_style,
_ => app.info_style,
},
)
});
let events: Vec<ListItem> = app
.events
.iter()
.map(|&(evt, level)| {
let s = match level {
"CRITICAL" => StyleDiff::default().fg(Color::Red),
"ERROR" => StyleDiff::default().fg(Color::Magenta),
"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)
.block(Block::default().borders(Borders::ALL).title("List"))
.start_corner(Corner::BottomLeft);

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

@ -4,13 +4,12 @@ mod util;
use crate::util::event::{Event, Events};
use std::{error::Error, io};
use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::AlternateScreen};
use tui::layout::Rect;
use tui::widgets::Clear;
use tui::{
backend::TermionBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, Paragraph, Text, Wrap},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, StyleDiff},
text::{Span, Spans},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
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);
long_line.push('\n');
let text = [
Text::raw("This is a line \n"),
Text::styled("This is a line \n", Style::default().fg(Color::Red)),
Text::styled("This is a line\n", Style::default().bg(Color::Blue)),
Text::styled(
let text = vec![
Spans::from("This is a line "),
Spans::from(Span::styled("This is a line ", StyleDiff::default().fg(Color::Red))),
Spans::from(Span::styled("This is a line", StyleDiff::default().bg(Color::Blue))),
Spans::from(Span::styled(
"This is a longer line\n",
Style::default().modifier(Modifier::CROSSED_OUT),
),
Text::styled(&long_line, Style::default().bg(Color::Green)),
Text::styled(
StyleDiff::default().modifier(Modifier::CROSSED_OUT),
)),
Spans::from(Span::styled(&long_line, StyleDiff::default().bg(Color::Green))),
Spans::from(Span::styled(
"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))
.alignment(Alignment::Left).wrap(Wrap { trim: true });
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))
.alignment(Alignment::Left).wrap(Wrap { trim: true });
f.render_widget(paragraph, chunks[1]);

@ -10,7 +10,8 @@ use termion::{event::Key, input::MouseTerminal, raw::IntoRawMode, screen::Altern
use tui::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
style::{Color, Modifier, Style, StyleDiff},
text::{Span, Spans},
widgets::{Block, Borders, Tabs},
Terminal,
};
@ -47,12 +48,23 @@ fn main() -> Result<(), Box<dyn Error>> {
let block = Block::default().style(Style::default().bg(Color::White));
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"))
.titles(&app.tabs.titles)
.select(app.tabs.index)
.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]);
let inner = match app.tabs.index {
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::{
backend::TermionBackend,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
widgets::{Block, Borders, List, Paragraph, Text},
style::{Color, Modifier, Style, StyleDiff},
text::{Span, Spans},
widgets::{Block, Borders, List, ListItem, Paragraph},
Terminal,
};
use unicode_width::UnicodeWidthStr;
@ -81,15 +82,27 @@ fn main() -> Result<(), Box<dyn Error>> {
.split(f.size());
let msg = match app.input_mode {
InputMode::Normal => "Press q to exit, e to start editing.",
InputMode::Editing => "Press Esc to stop editing, Enter to record the message",
InputMode::Normal => vec![
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 help_message = Paragraph::new(text.iter());
let text = vec![Spans::from(msg)];
let help_message = Paragraph::new(text);
f.render_widget(help_message, chunks[0]);
let text = [Text::raw(&app.input)];
let input = Paragraph::new(text.iter())
let text = vec![Spans::from(app.input.as_ref())];
let input = Paragraph::new(text)
.style(Style::default().fg(Color::Yellow))
.block(Block::default().borders(Borders::ALL).title("Input"));
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
.iter()
.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 =
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
f.render_widget(messages, chunks[2]);

@ -1,6 +1,7 @@
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Spans},
};
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
@ -298,6 +299,51 @@ impl Buffer {
(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) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {

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

@ -83,14 +83,17 @@ where
/// # use tui::Terminal;
/// # use tui::backend::TermionBackend;
/// # use tui::layout::Rect;
/// # use tui::widgets::{List, ListState, Text};
/// # use tui::widgets::{List, ListItem, ListState};
/// # let stdout = io::stdout();
/// # let backend = TermionBackend::new(stdout);
/// # let mut terminal = Terminal::new(backend).unwrap();
/// let mut state = ListState::default();
/// state.select(Some(1));
/// let items = vec![Text::raw("Item 1"), Text::raw("Item 2")];
/// let list = List::new(items.into_iter());
/// let items = vec![
/// ListItem::new("Item 1"),
/// ListItem::new("Item 2"),
/// ];
/// let list = List::new(items);
/// let area = Rect::new(0, 0, 5, 5);
/// let mut frame = terminal.get_frame();
/// 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> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let chart_area = match self.block {
Some(ref mut b) => {
let chart_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};

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

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

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

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

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

@ -15,9 +15,6 @@
//! - [`Sparkline`]
//! - [`Clear`]
use bitflags::bitflags;
use std::borrow::Cow;
mod barchart;
mod block;
pub mod canvas;
@ -36,15 +33,14 @@ pub use self::block::{Block, BorderType};
pub use self::chart::{Axis, Chart, Dataset, GraphType};
pub use self::clear::Clear;
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::sparkline::Sparkline;
pub use self::table::{Row, Table, TableState};
pub use self::tabs::Tabs;
use crate::buffer::Buffer;
use crate::layout::Rect;
use crate::style::Style;
use crate::{buffer::Buffer, layout::Rect};
use bitflags::bitflags;
bitflags! {
/// 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
pub trait Widget {
/// 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 tui::Terminal;
/// # 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.
/// struct Events {
@ -187,7 +167,7 @@ pub trait Widget {
/// terminal.draw(|f| {
/// // The items managed by the application are transformed to something
/// // 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.
/// let list = List::new(items);
/// // Finally the widget is rendered using the associated state. `events.state` is

@ -1,13 +1,16 @@
use either::Either;
use unicode_segmentation::UnicodeSegmentation;
use crate::{
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 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 {
match alignment {
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
///
/// ```
/// # use tui::widgets::{Block, Borders, Paragraph, Text, Wrap};
/// # use tui::style::{Style, Color};
/// # use tui::text::{Text, Spans, Span};
/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use tui::style::{Style, StyleDiff, Color, Modifier};
/// # use tui::layout::{Alignment};
/// let text = [
/// Text::raw("First line\n"),
/// Text::styled("Second line\n", Style::default().fg(Color::Red))
/// let text = vec![
/// Spans::from(vec![
/// 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))
/// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
/// ```
#[derive(Debug, Clone)]
pub struct Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
pub struct Paragraph<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Widget style
@ -46,9 +51,7 @@ where
/// How to wrap the text
wrap: Option<Wrap>,
/// The text to display
text: T,
/// Should we parse the text for embedded commands
raw: bool,
text: Text<'a>,
/// Scroll
scroll: (u16, u16),
/// Alignment of the text
@ -57,16 +60,17 @@ where
/// Describes how to wrap text across lines.
///
/// # Example
/// ## Examples
///
/// ```
/// # use tui::widgets::{Paragraph, Text, Wrap};
/// let bullet_points = [Text::raw(r#"Some indented points:
/// # use tui::widgets::{Paragraph, Wrap};
/// # use tui::text::Text;
/// let bullet_points = Text::from(r#"Some indented points:
/// - 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):
/// Paragraph::new(bullet_points.iter()).wrap(Wrap { trim: true });
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
/// // Some indented points:
/// // - First thing goes here and is
/// // long so that it wraps
@ -74,74 +78,67 @@ where
/// // is long enough to wrap
///
/// // 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:
/// // - First thing goes here
/// // and is long so that it wraps
/// // - Here is another point
/// // that is long enough to wrap
/// ```
#[derive(Debug, Clone, Copy)]
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,
}
impl<'a, 't, T> Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
pub fn new(text: T) -> Paragraph<'a, 't, T> {
impl<'a> Paragraph<'a> {
pub fn new<T>(text: T) -> Paragraph<'a>
where
T: Into<Text<'a>>,
{
Paragraph {
block: None,
style: Default::default(),
wrap: None,
raw: false,
text,
text: text.into(),
scroll: (0, 0),
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
}
pub fn style(mut self, style: Style) -> Paragraph<'a, 't, T> {
pub fn style(mut self, style: Style) -> Paragraph<'a> {
self.style = style;
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
}
pub fn raw(mut self, flag: bool) -> Paragraph<'a, 't, T> {
self.raw = flag;
self
}
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a, 't, T> {
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
self.scroll = offset;
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
}
}
impl<'a, 't, 'b, T> Widget for Paragraph<'a, 't, T>
where
T: Iterator<Item = &'t Text<'t>>,
{
impl<'a> Widget for Paragraph<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let text_area = match self.block {
Some(ref mut b) => {
let text_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
b.inner(area)
inner_area
}
None => area,
};
@ -153,15 +150,17 @@ where
buf.set_background(text_area, self.style.bg);
let style = self.style;
let mut styled = self.text.by_ref().flat_map(|t| match *t {
Text::Raw(ref d) => {
let data: &'t str = d; // coerce to &str
Either::Left(UnicodeSegmentation::graphemes(data, true).map(|g| Styled(g, style)))
}
Text::Styled(ref d, s) => {
let data: &'t str = d; // coerce to &str
Either::Right(UnicodeSegmentation::graphemes(data, true).map(move |g| Styled(g, s)))
}
let mut styled = self.text.lines.iter().flat_map(|spans| {
spans
.0
.iter()
.flat_map(|span| span.styled_graphemes(style))
// Required given the way composers work but might be refactored out if we change
// composers to operate on lines instead of a stream of graphemes.
.chain(iter::once(StyledGrapheme {
symbol: "\n",
style: self.style,
}))
});
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() {
if y >= self.scroll.0 {
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)
.set_symbol(if symbol.is_empty() {
// 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_width::UnicodeWidthStr;
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.
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
/// iterators for that).
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.
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,
current_line: Vec<Styled<'a>>,
next_line: Vec<Styled<'a>>,
current_line: Vec<StyledGrapheme<'a>>,
next_line: Vec<StyledGrapheme<'a>>,
/// Removes the leading whitespace from lines
trim: bool,
}
impl<'a, 'b> WordWrapper<'a, 'b> {
pub fn new(
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
trim: bool,
) -> WordWrapper<'a, 'b> {
@ -41,7 +38,7 @@ impl<'a, 'b> 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 {
return None;
}
@ -51,14 +48,14 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
let mut current_line_width = self
.current_line
.iter()
.map(|Styled(c, _)| c.width() as u16)
.map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
.sum();
let mut symbols_to_last_word_end: usize = 0;
let mut width_to_last_word_end: u16 = 0;
let mut prev_whitespace = false;
let mut symbols_exhausted = true;
for Styled(symbol, style) in &mut self.symbols {
for StyledGrapheme { symbol, style } in &mut self.symbols {
symbols_exhausted = false;
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;
}
self.current_line.push(Styled(symbol, style));
self.current_line.push(StyledGrapheme { symbol, style });
current_line_width += symbol.width() as u16;
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:
{
let remainder = &self.current_line[truncate_at..];
if let Some(remainder_nonwhite) = remainder
.iter()
.position(|Styled(c, _)| !c.chars().all(&char::is_whitespace))
if let Some(remainder_nonwhite) =
remainder.iter().position(|StyledGrapheme { symbol, .. }| {
!symbol.chars().all(&char::is_whitespace)
})
{
self.next_line
.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.
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,
current_line: Vec<Styled<'a>>,
current_line: Vec<StyledGrapheme<'a>>,
/// Record the offet to skip render
horizontal_offset: u16,
}
impl<'a, 'b> LineTruncator<'a, 'b> {
pub fn new(
symbols: &'b mut dyn Iterator<Item = Styled<'a>>,
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
max_line_width: u16,
) -> LineTruncator<'a, 'b> {
LineTruncator {
@ -152,7 +150,7 @@ impl<'a, 'b> 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 {
return None;
}
@ -163,7 +161,7 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
let mut skip_rest = false;
let mut symbols_exhausted = true;
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;
// 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;
self.current_line.push(Styled(symbol, style));
self.current_line.push(StyledGrapheme { symbol, style });
}
if skip_rest {
for Styled(symbol, _) in &mut self.symbols {
for StyledGrapheme { symbol, .. } in &mut self.symbols {
if symbol == "\n" {
break;
}
@ -243,7 +241,8 @@ mod test {
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
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 {
Composer::WordWrapper { 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() {
let line = styled
.iter()
.map(|Styled(g, _style)| *g)
.map(|StyledGrapheme { symbol, .. }| *symbol)
.collect::<String>();
assert!(width <= text_area_width);
lines.push(line);

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

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

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

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

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

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

Loading…
Cancel
Save