diff --git a/Cargo.toml b/Cargo.toml index 474ca19..6998738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/Makefile b/Makefile index ab680d9..13c5600 100644 --- a/Makefile +++ b/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 ================================= diff --git a/examples/block.rs b/examples/block.rs index d828bae..68d0b59 100644 --- a/examples/block.rs +++ b/examples/block.rs @@ -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> { .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()? { diff --git a/examples/chart.rs b/examples/chart.rs index 66a3e7a..100d44d 100644 --- a/examples/chart.rs +++ b/examples/chart.rs @@ -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> { .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> { .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]); })?; diff --git a/examples/demo/ui.rs b/examples/demo/ui.rs index b881251..19f7e3d 100644 --- a/examples/demo/ui.rs +++ b/examples/demo/ui.rs @@ -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(f: &mut Frame, 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 = 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 = 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(f: &mut Frame, 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); } diff --git a/examples/gauge.rs b/examples/gauge.rs index becf5c1..6705858 100644 --- a/examples/gauge.rs +++ b/examples/gauge.rs @@ -89,7 +89,7 @@ fn main() -> Result<(), Box> { .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> { .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]); })?; diff --git a/examples/list.rs b/examples/list.rs index b1b5153..a60d9d9 100644 --- a/examples/list.rs +++ b/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> { let style = Style::default().fg(Color::Black).bg(Color::White); - let items = app.items.items.iter().map(|i| Text::raw(*i)); + let items: Vec = 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 = 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); diff --git a/examples/paragraph.rs b/examples/paragraph.rs index 0ffafda..81fac4a 100644 --- a/examples/paragraph.rs +++ b/examples/paragraph.rs @@ -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> { ) .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]); diff --git a/examples/popup.rs b/examples/popup.rs index 6d8d995..57a52ee 100644 --- a/examples/popup.rs +++ b/examples/popup.rs @@ -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> { 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]); diff --git a/examples/tabs.rs b/examples/tabs.rs index 70a9a46..34a1ecd 100644 --- a/examples/tabs.rs +++ b/examples/tabs.rs @@ -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> { 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), diff --git a/examples/user_input.rs b/examples/user_input.rs index b170c1c..90b34c9 100644 --- a/examples/user_input.rs +++ b/examples/user_input.rs @@ -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> { .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> { } } - let messages = app + let messages: Vec = 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]); diff --git a/src/buffer.rs b/src/buffer.rs index 4b702fd..1deb13a 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -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() { diff --git a/src/lib.rs b/src/lib.rs index b45fa9d..e810e20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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}; diff --git a/src/terminal.rs b/src/terminal.rs index 54877ee..e93bda9 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -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); diff --git a/src/text.rs b/src/text.rs new file mode 100644 index 0000000..bc5f9f3 --- /dev/null +++ b/src/text.rs @@ -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(content: T) -> Span<'a> + where + T: Into>, + { + 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(content: T, style_diff: StyleDiff) -> Span<'a> + where + T: Into>, + { + 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::>() + /// ); + /// ``` + pub fn styled_graphemes( + &'a self, + base_style: Style, + ) -> impl Iterator> { + 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 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>); + +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 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>> for Spans<'a> { + fn from(spans: Vec>) -> Spans<'a> { + Spans(spans) + } +} + +impl<'a> From> for Spans<'a> { + fn from(span: Span<'a>) -> Spans<'a> { + Spans(vec![span]) + } +} + +impl<'a> From> 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>, +} + +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>> for Text<'a> { + fn from(lines: Vec>) -> Text<'a> { + Text { lines } + } +} diff --git a/src/widgets/barchart.rs b/src/widgets/barchart.rs index f31eba2..659db62 100644 --- a/src/widgets/barchart.rs +++ b/src/widgets/barchart.rs @@ -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, }; diff --git a/src/widgets/block.rs b/src/widgets/block.rs index 7fc4015..6cea485 100644 --- a/src/widgets/block.rs +++ b/src/widgets/block.rs @@ -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>, /// 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(mut self, title: T) -> Block<'a> + where + T: Into>, + { + 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); } } } diff --git a/src/widgets/canvas/mod.rs b/src/widgets/canvas/mod.rs index 3683bef..9ff7caa 100644 --- a/src/widgets/canvas/mod.rs +++ b/src/widgets/canvas/mod.rs @@ -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, }; diff --git a/src/widgets/chart.rs b/src/widgets/chart.rs index 32c9104..6f0ecf4 100644 --- a/src/widgets/chart.rs +++ b/src/widgets/chart.rs @@ -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 + 'a, -{ +pub struct Axis<'a> { /// Title displayed next to axis end - title: Option<&'a str>, - /// Style of the title - title_style: Style, + title: Option>, /// 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>>, /// The style used to draw the axis itself style: Style, } -impl<'a, L> Default for Axis<'a, L> -where - L: AsRef, -{ - 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, -{ - pub fn title(mut self, title: &'a str) -> Axis<'a, L> { - self.title = Some(title); +impl<'a> Axis<'a> { + pub fn title(mut self, title: T) -> Axis<'a> + where + T: Into>, + { + 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>) -> 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 + 'a, - LY: AsRef + 'a, -{ +pub struct Chart<'a> { /// A block to display around the widget eventually block: Option>, /// 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>, /// 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, - LY: AsRef, -{ - fn default() -> Chart<'a, LX, LY> { +impl<'a> Chart<'a> { + pub fn new(datasets: Vec>) -> 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, - LY: AsRef, -{ - 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 = 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, - LY: AsRef, -{ +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::>(); let cases = [ LegendTestCase { chart_area: Rect::new(0, 0, 100, 100), @@ -562,11 +535,16 @@ mod tests { }, ]; for case in &cases { - let chart: Chart = Chart::default() + let datasets = (0..10) + .map(|i| { + let name = format!("Dataset #{}", i); + Dataset::default().name(name).data(&data) + }) + .collect::>(); + 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); } diff --git a/src/widgets/gauge.rs b/src/widgets/gauge.rs index 7415665..5ff5c18 100644 --- a/src/widgets/gauge.rs +++ b/src/widgets/gauge.rs @@ -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>, ratio: f64, - label: Option<&'a str>, + label: Option>, 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(mut self, label: T) -> Gauge<'a> + where + T: Into>, + { + 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 diff --git a/src/widgets/list.rs b/src/widgets/list.rs index cc19428..1a9702f 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -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(content: T) -> ListItem<'a> + where + T: Into>, + { + 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>, -{ - block: Option>, - items: L, - start_corner: Corner, - /// Base style of the widget +pub struct List<'a> { + block: Option>, + items: Vec>, + /// 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> + Default, -{ - fn default() -> List<'b, L> { - List { - block: None, - items: L::default(), - style: Default::default(), - start_corner: Corner::TopLeft, - highlight_style: Style::default(), - highlight_symbol: None, - } - } -} - -impl<'b, L> List<'b, L> -where - L: Iterator>, -{ - pub fn new(items: L) -> List<'b, L> { +impl<'a> List<'a> { + pub fn new(items: T) -> List<'a> + where + T: Into>>, + { List { block: None, - items, - style: Default::default(), + style: Style::default(), + items: items.into(), start_corner: Corner::TopLeft, - highlight_style: Style::default(), + highlight_style_diff: StyleDiff::default(), highlight_symbol: None, } } - pub fn block(mut self, block: Block<'b>) -> List<'b, L> { + pub fn block(mut self, block: Block<'a>) -> List<'a> { self.block = Some(block); self } - pub fn items(mut self, items: I) -> List<'b, L> - where - I: IntoIterator, IntoIter = L>, - { - self.items = items.into_iter(); - self - } - - pub fn style(mut self, style: Style) -> List<'b, L> { + 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>, -{ +impl<'a> StatefulWidget for List<'a> { type State = ListState; fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - let list_area = match self.block { - Some(ref mut b) => { + let list_area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); b.render(area, buf); - b.inner(area) + inner_area } None => area, }; @@ -149,81 +148,113 @@ where return; } + buf.set_background(list_area, self.style.bg); + + if self.items.is_empty() { + return; + } let list_height = list_area.height as usize; - buf.set_background(list_area, self.style.bg); + let mut start = state.offset; + let mut end = state.offset; + let mut height = 0; + for item in self.items.iter().skip(state.offset) { + if height + item.height() > list_height { + break; + } + height += item.height(); + end += 1; + } + + let selected = state.selected.unwrap_or(0).min(self.items.len() - 1); + while selected >= end { + height = height.saturating_add(self.items[end].height()); + end += 1; + while height > list_height { + height = height.saturating_sub(self.items[start].height()); + start += 1; + } + } + while selected < start { + start -= 1; + height = height.saturating_add(self.items[start].height()); + while height > list_height { + end -= 1; + height = height.saturating_sub(self.items[end].height()); + } + } + state.offset = start; - // Use highlight_style only if something is selected - let (selected, highlight_style) = match state.selected { - Some(i) => (Some(i), self.highlight_style), - None => (None, self.style), - }; let highlight_symbol = self.highlight_symbol.unwrap_or(""); let blank_symbol = iter::repeat(" ") .take(highlight_symbol.width()) .collect::(); - // Make sure the list show the selected item - state.offset = if let Some(selected) = selected { - if selected >= list_height + state.offset - 1 { - selected + 1 - list_height - } else if selected < state.offset { - selected - } else { - state.offset - } - } else { - 0 - }; - + let mut current_height = 0; for (i, item) in self .items - .skip(state.offset) + .iter_mut() .enumerate() - .take(list_area.height as usize) + .skip(state.offset) + .take(end - start) { let (x, y) = match self.start_corner { - Corner::TopLeft => (list_area.left(), list_area.top() + i as u16), - Corner::BottomLeft => (list_area.left(), list_area.bottom() - (i + 1) as u16), - // Not supported - _ => (list_area.left(), list_area.top() + i as u16), + Corner::BottomLeft => { + current_height += item.height() as u16; + (list_area.left(), list_area.bottom() - current_height) + } + _ => { + let pos = (list_area.left(), list_area.top() + current_height); + current_height += item.height() as u16; + pos + } + }; + let area = Rect { + x, + y, + width: list_area.width, + height: item.height() as u16, }; - let (elem_x, style) = if let Some(s) = selected { - if s == i + state.offset { + let item_style = self.style.patch(item.style_diff); + buf.set_background(area, item_style.bg); + let elem_x = if let Some(s) = state.selected { + if s == i { + for line in &mut item.content.lines { + for span in &mut line.0 { + span.style_diff = span.style_diff.patch(self.highlight_style_diff); + } + } let (x, _) = buf.set_stringn( x, y, highlight_symbol, list_area.width as usize, - highlight_style, + item_style.patch(self.highlight_style_diff), ); - (x, Some(highlight_style)) + x } else { let (x, _) = - buf.set_stringn(x, y, &blank_symbol, list_area.width as usize, self.style); - (x, None) + buf.set_stringn(x, y, &blank_symbol, list_area.width as usize, item_style); + x } } else { - (x, None) + x }; - let max_element_width = (list_area.width - (elem_x - x)) as usize; - match item { - Text::Raw(ref v) => { - buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(self.style)); - } - Text::Styled(ref v, s) => { - buf.set_stringn(elem_x, y, v, max_element_width, style.unwrap_or(s)); - } - }; + for (j, line) in item.content.lines.iter().enumerate() { + buf.set_spans( + elem_x, + y + j as u16, + line, + max_element_width as u16, + self.style, + ); + } } } } -impl<'b, L> Widget for List<'b, L> -where - L: Iterator>, -{ +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); diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 0bb1fa8..99a3c9f 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -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>>(data: D) -> Text<'b> { - Text::Raw(data.into()) - } - - pub fn styled>>(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= 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 diff --git a/src/widgets/paragraph.rs b/src/widgets/paragraph.rs index 47d2ccd..91bf622 100644 --- a/src/widgets/paragraph.rs +++ b/src/widgets/paragraph.rs @@ -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>, -{ +pub struct Paragraph<'a> { /// A block to wrap the widget in block: Option>, /// Widget style @@ -46,9 +51,7 @@ where /// How to wrap the text wrap: Option, /// 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>, -{ - pub fn new(text: T) -> Paragraph<'a, 't, T> { +impl<'a> Paragraph<'a> { + pub fn new(text: T) -> Paragraph<'a> + where + T: Into>, + { 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>, -{ +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 = 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 diff --git a/src/widgets/reflow.rs b/src/widgets/reflow.rs index d1ff8a3..e5f8e2a 100644 --- a/src/widgets/reflow.rs +++ b/src/widgets/reflow.rs @@ -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>, + symbols: &'b mut dyn Iterator>, max_line_width: u16, - current_line: Vec>, - next_line: Vec>, + current_line: Vec>, + next_line: Vec>, /// Removes the leading whitespace from lines trim: bool, } impl<'a, 'b> WordWrapper<'a, 'b> { pub fn new( - symbols: &'b mut dyn Iterator>, + symbols: &'b mut dyn Iterator>, 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>, + symbols: &'b mut dyn Iterator>, max_line_width: u16, - current_line: Vec>, + current_line: Vec>, /// Record the offet to skip render horizontal_offset: u16, } impl<'a, 'b> LineTruncator<'a, 'b> { pub fn new( - symbols: &'b mut dyn Iterator>, + symbols: &'b mut dyn Iterator>, 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, Vec) { 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 = 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::(); assert!(width <= text_area_width); lines.push(line); diff --git a/src/widgets/sparkline.rs b/src/widgets/sparkline.rs index a5718a4..1dab27d 100644 --- a/src/widgets/sparkline.rs +++ b/src/widgets/sparkline.rs @@ -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, }; diff --git a/src/widgets/table.rs b/src/widgets/table.rs index 27e0677..0bf4395 100644 --- a/src/widgets/table.rs +++ b/src/widgets/table.rs @@ -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, }; diff --git a/src/widgets/tabs.rs b/src/widgets/tabs.rs index e03e494..705a8bf 100644 --- a/src/widgets/tabs.rs +++ b/src/widgets/tabs.rs @@ -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 + 'a, -{ +pub struct Tabs<'a> { /// A block to wrap this widget in if necessary block: Option>, /// One title for each tab - titles: &'a [T], + titles: Vec>, /// 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, -{ - fn default() -> Tabs<'a, T> { +impl<'a> Tabs<'a> { + pub fn new(titles: Vec>) -> 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, -{ - pub fn block(mut self, block: Block<'a>) -> Tabs<'a, T> { + pub fn block(mut self, block: Block<'a>) -> Tabs<'a> { self.block = Some(block); self } - pub fn titles(mut self, titles: &'a [T]) -> Tabs<'a, T> { - self.titles = titles; + pub fn select(mut self, selected: usize) -> Tabs<'a> { + self.selected = selected; self } - pub fn select(mut self, selected: usize) -> Tabs<'a, T> { - self.selected = selected; + pub fn style(mut self, style: Style) -> Tabs<'a> { + self.style = style; self } - pub fn style(mut self, style: Style) -> Tabs<'a, T> { - self.style = style; + #[deprecated(since = "0.10.0", note = "You should use `Tabs::highlight_style_diff`")] + pub fn highlight_style(mut self, style: Style) -> Tabs<'a> { + self.highlight_style_diff = StyleDiff::from(style); self } - pub fn highlight_style(mut self, style: Style) -> Tabs<'a, T> { - self.highlight_style = style; + pub fn highlight_style_diff(mut self, diff: StyleDiff) -> Tabs<'a> { + self.highlight_style_diff = diff; self } - pub fn divider(mut self, divider: &'a str) -> Tabs<'a, T> { - self.divider = divider; + pub fn divider(mut self, divider: T) -> Tabs<'a> + where + T: Into>, + { + self.divider = divider.into(); self } } -impl<'a, T> Widget for Tabs<'a, T> -where - T: AsRef, -{ +impl<'a> Widget for Tabs<'a> { fn render(mut self, area: Rect, buf: &mut Buffer) { - let tabs_area = match self.block { - Some(ref mut b) => { + let tabs_area = match self.block.take() { + Some(b) => { + let inner_area = b.inner(area); b.render(area, buf); - b.inner(area) + inner_area } None => area, }; @@ -112,28 +105,32 @@ where let mut x = tabs_area.left(); let titles_length = self.titles.len(); - let divider_width = self.divider.width() as u16; - for (title, style, last_title) in self.titles.iter().enumerate().map(|(i, t)| { - let lt = i + 1 == titles_length; + for (i, mut title) in self.titles.into_iter().enumerate() { + let last_title = titles_length - 1 == i; if i == self.selected { - (t, self.highlight_style, lt) - } else { - (t, self.style, lt) + for span in &mut title.0 { + span.style_diff = span.style_diff.patch(self.highlight_style_diff); + } } - }) { - x += 1; - if x >= tabs_area.right() { + x = x.saturating_add(1); + let remaining_width = tabs_area.right().saturating_sub(x); + if remaining_width == 0 { + break; + } + let pos = buf.set_spans(x, tabs_area.top(), &title, remaining_width, self.style); + x = pos.0.saturating_add(1); + let remaining_width = tabs_area.right().saturating_sub(x); + if remaining_width == 0 || last_title { break; - } else { - buf.set_string(x, tabs_area.top(), title.as_ref(), style); - x += title.as_ref().width() as u16 + 1; - if x >= tabs_area.right() || last_title { - break; - } else { - buf.set_string(x, tabs_area.top(), self.divider, self.style); - x += divider_width; - } } + let pos = buf.set_span( + x, + tabs_area.top(), + &self.divider, + remaining_width, + self.style, + ); + x = pos.0; } } } diff --git a/tests/widgets_block.rs b/tests/widgets_block.rs index 3adbd3b..120530b 100644 --- a/tests/widgets_block.rs +++ b/tests/widgets_block.rs @@ -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 { diff --git a/tests/widgets_chart.rs b/tests/widgets_chart.rs index dd53701..164118f 100644 --- a/tests/widgets_chart.rs +++ b/tests/widgets_chart.rs @@ -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> { + 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 { diff --git a/tests/widgets_list.rs b/tests/widgets_list.rs index d04cf77..b59e331 100644 --- a/tests/widgets_list.rs +++ b/tests/widgets_list.rs @@ -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, - items: Vec>, + items: Vec>, 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); diff --git a/tests/widgets_paragraph.rs b/tests/widgets_paragraph.rs index 0bc7083..9c556eb 100644 --- a/tests/widgets_paragraph.rs +++ b/tests/widgets_paragraph.rs @@ -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); diff --git a/tests/widgets_tabs.rs b/tests/widgets_tabs.rs index 67e435d..c2d7edf 100644 --- a/tests/widgets_tabs.rs +++ b/tests/widgets_tabs.rs @@ -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); }