mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-27 13:01:13 +00:00
feat(text): add new text primitives
This commit is contained in:
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 }
|
||||
|
11
Makefile
11
Makefile
@ -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]);
|
||||
})?;
|
||||
|
||||
|
106
examples/list.rs
106
examples/list.rs
@ -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);
|
||||
|
307
src/text.rs
Normal file
307
src/text.rs
Normal file
@ -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> {
|
||||
impl<'a> List<'a> {
|
||||
pub fn new<T>(items: T) -> List<'a>
|
||||
where
|
||||
T: Into<Vec<ListItem<'a>>>,
|
||||
{
|
||||
List {
|
||||
block: None,
|
||||
items: L::default(),
|
||||
style: Default::default(),
|
||||
style: Style::default(),
|
||||
items: items.into(),
|
||||
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 {
|
||||
block: None,
|
||||
items,
|
||||
style: Default::default(),
|
||||
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;
|
||||
}
|
||||
|
||||
let list_height = list_area.height as usize;
|
||||
|
||||
buf.set_background(list_area, self.style.bg);
|
||||
|
||||
// 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),
|
||||
};
|
||||
if self.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let list_height = list_area.height as usize;
|
||||
|
||||
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;
|
||||
|
||||
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 (elem_x, style) = if let Some(s) = selected {
|
||||
if s == i + state.offset {
|
||||
let area = Rect {
|
||||
x,
|
||||
y,
|
||||
width: list_area.width,
|
||||
height: item.height() as u16,
|
||||
};
|
||||
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;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn select(mut self, selected: usize) -> Tabs<'a, T> {
|
||||
pub fn select(mut self, selected: usize) -> Tabs<'a> {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: Style) -> Tabs<'a, T> {
|
||||
pub fn style(mut self, style: Style) -> Tabs<'a> {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn highlight_style(mut self, style: Style) -> Tabs<'a, T> {
|
||||
self.highlight_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 divider(mut self, divider: &'a str) -> Tabs<'a, T> {
|
||||
self.divider = divider;
|
||||
pub fn highlight_style_diff(mut self, diff: StyleDiff) -> Tabs<'a> {
|
||||
self.highlight_style_diff = diff;
|
||||
self
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}) {
|
||||
x += 1;
|
||||
if x >= tabs_area.right() {
|
||||
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;
|
||||
for span in &mut title.0 {
|
||||
span.style_diff = span.style_diff.patch(self.highlight_style_diff);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
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…
x
Reference in New Issue
Block a user