Compare commits

..

1 Commits

Author SHA1 Message Date
Josh McKinney
f99c224fc1 feat: impl WidgetRef for Fn(Rect, &mut Buffer)
This allows you to treat any function  that takes a `Rect` and a mutable
reference a `Buffer` as a widget in situations where you don't want to
create a new type for a widget.

Example:

```rust
fn hello(area: Rect, buf: &mut Buffer) {
    Line::raw("Hello").render(area, buf);
}

frame.render_widget(&hello, frame.size());
frame.render_widget_ref(hello, frame.size());
```

Related to: <https://forum.ratatui.rs/t/idea-functionwidget-was-thoughts-on-tui-react/59/2>
2024-05-29 16:44:01 -07:00
53 changed files with 769 additions and 2179 deletions

View File

@@ -11,7 +11,6 @@ GitHub with a [breaking change] label.
This is a quick summary of the sections below:
- [Unreleased](#unreleased)
- 'termion' updated to 4.0
- `Rect::inner` takes `Margin` directly instead of reference
- `Buffer::filled` takes `Cell` directly instead of reference
- `Stylize::bg()` now accepts `Into<Color>`
@@ -55,63 +54,6 @@ This is a quick summary of the sections below:
## Unreleased
### Prelude items added / removed ([#1149])
The following items have been removed from the prelude:
- `style::Styled` - this trait is useful for widgets that want to
support the Stylize trait, but it adds complexity as widgets have two
`style` methods and a `set_style` method.
- `symbols::Marker` - this item is used by code that needs to draw to
the `Canvas` widget, but it's not a common item that would be used by
most users of the library.
- `terminal::{CompletedFrame, TerminalOptions, Viewport}` - these items
are rarely used by code that needs to interact with the terminal, and
they're generally only ever used once in any app.
The following items have been added to the prelude:
- `layout::{Position, Size}` - these items are used by code that needs
to interact with the layout system. These are newer items that were
added in the last few releases, which should be used more liberally.
This may cause conflicts for types defined elsewhere with a similar
name.
To update your app:
```diff
// if your app uses Styled::style() or Styled::set_style():
-use ratatui::prelude::*;
+use ratatui::{prelude::*, style::Styled};
// if your app uses symbols::Marker:
-use ratatui::prelude::*;
+use ratatui::{prelude::*, symbols::Marker}
// if your app uses terminal::{CompletedFrame, TerminalOptions, Viewport}
-use ratatui::prelude::*;
+use ratatui::{prelude::*, terminal::{CompletedFrame, TerminalOptions, Viewport}};
// to disambiguate existing types named Position or Size:
- use some_crate::{Position, Size};
- let size: Size = ...;
- let position: Position = ...;
+ let size: some_crate::Size = ...;
+ let position: some_crate::Position = ...;
```
[#1149]: https://github.com/ratatui-org/ratatui/pull/1149
### Termion is updated to 4.0 [#1106]
Changelog: <https://gitlab.redox-os.org/redox-os/termion/-/blob/master/CHANGELOG.md>
A change is only necessary if you were matching on all variants of the `MouseEvent` enum without a
wildcard. In this case, you need to either handle the two new variants, `MouseLeft` and
`MouseRight`, or add a wildcard.
[#1106]: https://github.com/ratatui-org/ratatui/pull/1106
### `Rect::inner` takes `Margin` directly instead of reference ([#1008])
[#1008]: https://github.com/ratatui-org/ratatui/pull/1008
@@ -144,9 +86,9 @@ wildcard. In this case, you need to either handle the two new variants, `MouseLe
Previously, `Stylize::bg()` accepted `Color` but now accepts `Into<Color>`. This allows more
flexible types from calling scopes, though it can break some type inference in the calling scope.
### Remove deprecated `List::start_corner` and `layout::Corner` ([#759])
### Remove deprecated `List::start_corner` and `layout::Corner` ([#757])
[#759]: https://github.com/ratatui-org/ratatui/pull/759
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`List::start_corner` was deprecated in v0.25. Use `List::direction` and `ListDirection` instead.
@@ -252,6 +194,8 @@ The following example shows how to migrate for `Line`, but the same applies for
### Remove deprecated `Block::title_on_bottom` ([#757])
[#757]: https://github.com/ratatui-org/ratatui/pull/757
`Block::title_on_bottom` was deprecated in v0.22. Use `Block::title` and `Title::position` instead.
```diff
@@ -507,8 +451,8 @@ The MSRV of ratatui is now 1.67 due to an MSRV update in a dependency (`time`).
[#205]: https://github.com/ratatui-org/ratatui/issues/205
The `serde` representation of `bitflags` has changed. Any existing serialized types that have
Borders or Modifiers will need to be re-serialized. This is documented in the [`bitflags`
The `serde` representation of `bitflags` has changed. Any existing serialized types that have Borders or
Modifiers will need to be re-serialized. This is documented in the [`bitflags`
changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md#200-rc2)..
## [v0.21.0](https://github.com/ratatui-org/ratatui/releases/tag/v0.21.0)

View File

@@ -30,20 +30,18 @@ cassowary = "0.3"
compact_str = "0.7.1"
crossterm = { version = "0.27", optional = true }
document-features = { version = "0.2.7", optional = true }
itertools = "0.13"
itertools = "0.12"
lru = "0.12.0"
paste = "1.0.2"
palette = { version = "0.7.6", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
stability = "0.2.0"
strum = { version = "0.26", features = ["derive"] }
strum_macros = { version = "0.26.3" }
termion = { version = "4.0.0", optional = true }
termion = { version = "3.0", optional = true }
termwiz = { version = "0.22.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-truncate = "1"
unicode-width = "0.1.13"
unicode-width = "0.1"
[dev-dependencies]
anyhow = "1.0.71"
@@ -59,11 +57,8 @@ palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
rstest = "0.21.0"
rstest = "0.19.0"
serde_json = "1.0.109"
tracing = "0.1.40"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[lints.rust]
unsafe_code = "forbid"
@@ -126,9 +121,6 @@ serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
## enables the [`border!`] macro.
macros = []
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
palette = ["dep:palette"]
## enables all widgets.
all-widgets = ["widget-calendar"]
@@ -294,11 +286,6 @@ name = "line_gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hyperlink"
required-features = ["crossterm", "unstable-widget-ref"]
doc-scrape-examples = true
[[example]]
name = "list"
required-features = ["crossterm"]
@@ -356,11 +343,6 @@ name = "tabs"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "tracing"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "user_input"
required-features = ["crossterm"]

View File

@@ -25,10 +25,10 @@
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
[![Forum Badge]][Forum]<br>
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -67,7 +67,6 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
- [API Docs] - the full API documentation for the library on docs.rs.
- [Examples] - a collection of examples that demonstrate how to use the library.
- [Contributing] - Please read this if you are interested in contributing to the project.
@@ -340,8 +339,6 @@ Running this example produces the following output:
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
[Forum]: https://forum.ratatui.rs
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
<!-- cargo-rdme end -->
@@ -357,10 +354,9 @@ In order to organize ourselves, we currently use a [Discord server](https://disc
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
Pull Request].
While we do utilize Discord for coordinating, it's not essential for contributing.
Our primary open-source workflow is centered around GitHub.
For significant discussions, we rely on GitHub — please open an issue, a discussion or a PR.
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
you are interested in working on a PR or issue opened in the previous repository.

View File

@@ -37,12 +37,16 @@ use palette::{convert::FromColorUnclamped, Okhsv, Srgb};
use ratatui::{
backend::{Backend, CrosstermBackend},
buffer::Buffer,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
},
layout::{Constraint, Layout, Rect},
style::Color,
terminal::Terminal,
text::Text,
widgets::Widget,
Terminal,
};
#[derive(Debug, Default)]
@@ -97,9 +101,9 @@ struct ColorsWidget {
fn main() -> Result<()> {
install_error_hooks()?;
let backend = CrosstermBackend::stdout()?;
let terminal = Terminal::new(backend)?;
let terminal = init_terminal()?;
App::default().run(terminal)?;
restore_terminal()?;
Ok(())
}
@@ -268,12 +272,27 @@ fn install_error_hooks() -> Result<()> {
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
eyre::set_hook(Box::new(move |e| {
let _ = CrosstermBackend::restore(stdout());
let _ = restore_terminal();
error(e)
}))?;
panic::set_hook(Box::new(move |info| {
let _ = CrosstermBackend::restore(stdout());
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -122,8 +122,8 @@ pub struct TabsState<'a> {
}
impl<'a> TabsState<'a> {
pub fn new(titles: Vec<&'a str>) -> Self {
Self { titles, index: 0 }
pub fn new(titles: Vec<&'a str>) -> TabsState {
TabsState { titles, index: 0 }
}
pub fn next(&mut self) {
self.index = (self.index + 1) % self.titles.len();

View File

@@ -1,151 +0,0 @@
//! # [Ratatui] Hyperlink examplew
//!
//! Shows how to use [OSC 8] to create hyperlinks in the terminal.
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self, stdout, Stdout},
panic,
};
use color_eyre::{
config::{EyreHook, HookBuilder, PanicHook},
eyre, Result,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::WidgetRef};
fn main() -> Result<()> {
init_error_handling()?;
let mut terminal = init_terminal()?;
let app = App::new();
app.run(&mut terminal)?;
restore_terminal()?;
Ok(())
}
struct App {
hyperlink: Hyperlink<'static>,
}
impl App {
fn new() -> Self {
let text = Line::from(vec!["Example ".into(), "hyperlink".blue()]);
let hyperlink = Hyperlink::new(text, "https://example.com");
Self { hyperlink }
}
fn run(self, terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> io::Result<()> {
loop {
terminal.draw(|frame| frame.render_widget(&self.hyperlink, frame.size()))?;
if let Event::Key(key) = event::read()? {
if matches!(key.code, KeyCode::Char('q') | KeyCode::Esc) {
break;
}
}
}
Ok(())
}
}
/// A hyperlink widget that renders a hyperlink in the terminal using [OSC 8].
///
/// [OSC 8]: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
struct Hyperlink<'content> {
text: Text<'content>,
url: String,
}
impl<'content> Hyperlink<'content> {
fn new(text: impl Into<Text<'content>>, url: impl Into<String>) -> Self {
Self {
text: text.into(),
url: url.into(),
}
}
}
impl WidgetRef for Hyperlink<'_> {
fn render_ref(&self, area: Rect, buffer: &mut Buffer) {
self.text.render_ref(area, buffer);
// this is a hacky workaround for https://github.com/ratatui-org/ratatui/issues/902, a bug
// in the terminal code that incorrectly calculates the width of ANSI escape sequences. It
// works by rendering the hyperlink as a series of 2-character chunks, which is the
// calculated width of the hyperlink text.
for (i, two_chars) in self
.text
.to_string()
.chars()
.chunks(2)
.into_iter()
.enumerate()
{
let text = two_chars.collect::<String>();
let hyperlink = format!("\x1B]8;;{}\x07{}\x1B]8;;\x07", self.url, text);
buffer
.get_mut(area.x + i as u16 * 2, area.y)
.set_symbol(hyperlink.as_str());
}
}
}
/// Initialize the terminal with raw mode and alternate screen.
fn init_terminal() -> io::Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
Terminal::new(CrosstermBackend::new(stdout()))
}
/// Restore the terminal to its original state.
fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
execute!(stdout(), LeaveAlternateScreen)?;
Ok(())
}
/// Initialize error handling with color-eyre.
pub fn init_error_handling() -> Result<()> {
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
set_panic_hook(panic_hook);
set_error_hook(eyre_hook)?;
Ok(())
}
/// Install a panic hook that restores the terminal before printing the panic.
fn set_panic_hook(panic_hook: PanicHook) {
let panic_hook = panic_hook.into_panic_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore_terminal();
panic_hook(panic_info);
}));
}
/// Install an error hook that restores the terminal before printing the error.
fn set_error_hook(eyre_hook: EyreHook) -> Result<()> {
let eyre_hook = eyre_hook.into_eyre_hook();
eyre::set_hook(Box::new(move |error| {
let _ = restore_terminal();
eyre_hook(error)
}))?;
Ok(())
}

View File

@@ -15,7 +15,11 @@
use ratatui::{
backend::CrosstermBackend,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
text::Text,
Terminal,
};
@@ -27,9 +31,9 @@ use ratatui::{
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
let backend = CrosstermBackend::stdout()?;
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
enable_raw_mode()?;
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
loop {
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.size()))?;
if let Event::Key(key) = event::read()? {
@@ -38,5 +42,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -14,218 +14,154 @@
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use std::{
io::{self},
error::Error,
io,
time::{Duration, Instant},
};
use crossterm::event::KeyEventKind;
use ratatui::{
buffer::Buffer,
crossterm::event::{self, Event, KeyCode},
layout::{Constraint, Layout, Rect},
style::{Color, Stylize},
backend::{Backend, CrosstermBackend},
crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
},
layout::{Constraint, Layout},
style::{Color, Modifier, Style, Stylize},
terminal::{Frame, Terminal},
text::{Line, Masked, Span},
widgets::{Block, Paragraph, Widget, Wrap},
widgets::{Block, Paragraph, Wrap},
};
use self::common::{init_terminal, install_hooks, restore_terminal, Tui};
fn main() -> color_eyre::Result<()> {
install_hooks()?;
let mut terminal = init_terminal()?;
let mut app = App::new();
app.run(&mut terminal)?;
restore_terminal()?;
Ok(())
}
#[derive(Debug)]
struct App {
should_exit: bool,
scroll: u16,
last_tick: Instant,
}
impl App {
/// The duration between each tick.
const TICK_RATE: Duration = Duration::from_millis(250);
/// Create a new instance of the app.
fn new() -> Self {
Self {
should_exit: false,
scroll: 0,
last_tick: Instant::now(),
}
const fn new() -> Self {
Self { scroll: 0 }
}
/// Run the app until the user exits.
fn run(&mut self, terminal: &mut Tui) -> io::Result<()> {
while !self.should_exit {
self.draw(terminal)?;
self.handle_events()?;
if self.last_tick.elapsed() >= Self::TICK_RATE {
self.on_tick();
self.last_tick = Instant::now();
}
}
Ok(())
fn on_tick(&mut self) {
self.scroll += 1;
self.scroll %= 10;
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
}
/// Draw the app to the terminal.
fn draw(&mut self, terminal: &mut Tui) -> io::Result<()> {
terminal.draw(|frame| frame.render_widget(self, frame.size()))?;
Ok(())
}
Ok(())
}
/// Handle events from the terminal.
fn handle_events(&mut self) -> io::Result<()> {
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
while event::poll(timeout)? {
fn run_app<B: Backend>(
terminal: &mut Terminal<B>,
mut app: App,
tick_rate: Duration,
) -> io::Result<()> {
let mut last_tick = Instant::now();
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
self.should_exit = true;
if key.code == KeyCode::Char('q') {
return Ok(());
}
}
}
Ok(())
}
/// Update the app state on each tick.
fn on_tick(&mut self) {
self.scroll = (self.scroll + 1) % 10;
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
}
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), no wrap"))
.gray()
.render(areas[0], buf);
Paragraph::new(create_lines(area))
.block(title_block("Default alignment (Left), with wrap"))
.gray()
.wrap(Wrap { trim: true })
.render(areas[1], buf);
Paragraph::new(create_lines(area))
.block(title_block("Right alignment, with wrap"))
.gray()
.right_aligned()
.wrap(Wrap { trim: true })
.render(areas[2], buf);
Paragraph::new(create_lines(area))
.block(title_block("Center alignment, with wrap, with scroll"))
.gray()
.centered()
.wrap(Wrap { trim: true })
.scroll((self.scroll, 0))
.render(areas[3], buf);
}
}
fn ui(f: &mut Frame, app: &App) {
let size = f.size();
/// Create a bordered block with a title.
fn title_block(title: &str) -> Block {
Block::bordered()
.gray()
.title(title.bold().into_centered_line())
}
// Words made "loooong" to demonstrate line breaking.
let s = "Veeeeeeeeeeeeeeeery loooooooooooooooooong striiiiiiiiiiiiiiiiiiiiiiiiiing. ";
let mut long_line = s.repeat(usize::from(size.width) / s.len() + 4);
long_line.push('\n');
/// Create some lines to display in the paragraph.
fn create_lines(area: Rect) -> Vec<Line<'static>> {
let short_line = "A long line to demonstrate line wrapping. ";
let long_line = short_line.repeat(usize::from(area.width) / short_line.len() + 4);
let mut styled_spans = vec![];
for span in [
"Styled".blue(),
"Spans".red().on_white(),
"Bold".bold(),
"Italic".italic(),
"Underlined".underlined(),
"Strikethrough".crossed_out(),
] {
styled_spans.push(span);
styled_spans.push(" ".into());
}
vec![
Line::raw("Unstyled Line"),
Line::raw("Styled Line").black().on_red().bold().italic(),
Line::from(styled_spans),
Line::from(long_line.green().italic()),
Line::from_iter([
let block = Block::new().black();
f.render_widget(block, size);
let layout = Layout::vertical([Constraint::Ratio(1, 4); 4]).split(size);
let text = vec![
Line::from("This is a line "),
Line::from("This is a line ".red()),
Line::from("This is a line".on_blue()),
Line::from("This is a longer line".crossed_out()),
Line::from(long_line.on_green()),
Line::from("This is a line".green().italic()),
Line::from(vec![
"Masked text: ".into(),
Span::styled(Masked::new("my secret password", '*'), Color::Red),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
]),
]
}
/// A module for common functionality used in the examples.
mod common {
use std::{
io::{self, stdout, Stdout},
panic,
};
use color_eyre::{
config::{EyreHook, HookBuilder, PanicHook},
eyre,
};
use crossterm::ExecutableCommand;
use ratatui::{
backend::CrosstermBackend,
crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
},
terminal::Terminal,
};
// A simple alias for the terminal type used in this example.
pub type Tui = Terminal<CrosstermBackend<Stdout>>;
/// Initialize the terminal and enter alternate screen mode.
pub fn init_terminal() -> io::Result<Tui> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
Terminal::new(backend)
}
/// Restore the terminal to its original state.
pub fn restore_terminal() -> io::Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
Ok(())
}
/// Installs hooks for panic and error handling.
///
/// Makes the app resilient to panics and errors by restoring the terminal before printing the
/// panic or error message. This prevents error messages from being messed up by the terminal
/// state.
pub fn install_hooks() -> color_eyre::Result<()> {
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
install_panic_hook(panic_hook);
install_error_hook(eyre_hook)?;
Ok(())
}
/// Install a panic hook that restores the terminal before printing the panic.
fn install_panic_hook(panic_hook: PanicHook) {
let panic_hook = panic_hook.into_panic_hook();
panic::set_hook(Box::new(move |panic_info| {
let _ = restore_terminal();
panic_hook(panic_info);
}));
}
/// Install an error hook that restores the terminal before printing the error.
fn install_error_hook(eyre_hook: EyreHook) -> color_eyre::Result<()> {
let eyre_hook = eyre_hook.into_eyre_hook();
eyre::set_hook(Box::new(move |error| {
let _ = restore_terminal();
eyre_hook(error)
}))?;
Ok(())
}
];
let create_block = |title| {
Block::bordered()
.style(Style::default().fg(Color::Gray))
.title(Span::styled(
title,
Style::default().add_modifier(Modifier::BOLD),
))
};
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), no wrap"));
f.render_widget(paragraph, layout[0]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Default alignment (Left), with wrap"))
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[1]);
let paragraph = Paragraph::new(text.clone())
.style(Style::default().fg(Color::Gray))
.block(create_block("Right alignment, with wrap"))
.right_aligned()
.wrap(Wrap { trim: true });
f.render_widget(paragraph, layout[2]);
let paragraph = Paragraph::new(text)
.style(Style::default().fg(Color::Gray))
.block(create_block("Center alignment, with wrap, with scroll"))
.centered()
.wrap(Wrap { trim: true })
.scroll((app.scroll, 0));
f.render_widget(paragraph, layout[3]);
}

View File

@@ -1,154 +0,0 @@
//! # [Ratatui] Tracing example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
// A simple example demonstrating how to use the [tracing] with Ratatui to log to a file.
//
// This example demonstrates how to use the [tracing] crate with Ratatui to log to a file. The
// example sets up a simple logger that logs to a file named `tracing.log` in the current directory.
//
// Run the example with `cargo run --example tracing` and then view the `tracing.log` file to see
// the logs. To see more logs, you can run the example with `RUST_LOG=tracing=debug cargo run
// --example`
//
// For a helpful widget that handles logging, see the [tui-logger] crate.
//
// [tracing]: https://crates.io/crates/tracing
// [tui-logger]: https://crates.io/crates/tui-logger
use std::{fs::File, io::stdout, panic, time::Duration};
use color_eyre::{
config::HookBuilder,
eyre::{self, Context},
Result,
};
use crossterm::{
event::{self, Event, KeyCode},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
backend::{Backend, CrosstermBackend},
terminal::Terminal,
widgets::{Block, Paragraph},
};
use tracing::{debug, info, instrument, trace, Level};
use tracing_appender::{non_blocking, non_blocking::WorkerGuard};
use tracing_subscriber::EnvFilter;
fn main() -> Result<()> {
init_error_hooks()?;
let _guard = init_tracing()?;
info!("Starting tracing example");
let mut terminal = init_terminal()?;
let mut events = vec![]; // a buffer to store the recent events to display in the UI
while !should_exit(&events) {
handle_events(&mut events)?;
terminal.draw(|frame| ui(frame, &events))?;
}
restore_terminal()?;
info!("Exiting tracing example");
println!("See the tracing.log file for the logs");
Ok(())
}
fn should_exit(events: &[Event]) -> bool {
events
.iter()
.any(|event| matches!(event, Event::Key(key) if key.code == KeyCode::Char('q')))
}
/// Handle events and insert them into the events vector keeping only the last 10 events
#[instrument(skip(events))]
fn handle_events(events: &mut Vec<Event>) -> Result<()> {
// Render the UI at least once every 100ms
if event::poll(Duration::from_millis(100))? {
let event = event::read()?;
debug!(?event);
events.insert(0, event);
}
events.truncate(10);
Ok(())
}
#[instrument(skip_all)]
fn ui(frame: &mut ratatui::Frame, events: &[Event]) {
// To view this event, run the example with `RUST_LOG=tracing=debug cargo run --example tracing`
trace!(frame_count = frame.count(), event_count = events.len());
let area = frame.size();
let events = events.iter().map(|e| format!("{e:?}")).collect::<Vec<_>>();
let paragraph = Paragraph::new(events.join("\n"))
.block(Block::bordered().title("Tracing example. Press 'q' to quit."));
frame.render_widget(paragraph, area);
}
/// Initialize the tracing subscriber to log to a file
///
/// This function initializes the tracing subscriber to log to a file named `tracing.log` in the
/// current directory. The function returns a [`WorkerGuard`] that must be kept alive for the
/// duration of the program to ensure that logs are flushed to the file on shutdown. The logs are
/// written in a non-blocking fashion to ensure that the logs do not block the main thread.
fn init_tracing() -> Result<WorkerGuard> {
let file = File::create("tracing.log").wrap_err("failed to create tracing.log")?;
let (non_blocking, guard) = non_blocking(file);
// By default, the subscriber is configured to log all events with a level of `DEBUG` or higher,
// but this can be changed by setting the `RUST_LOG` environment variable.
let env_filter = EnvFilter::builder()
.with_default_directive(Level::DEBUG.into())
.from_env_lossy();
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_env_filter(env_filter)
.init();
Ok(guard)
}
/// Initialize the error hooks to ensure that the terminal is restored to a sane state before
/// exiting
fn init_error_hooks() -> Result<()> {
let (panic, error) = HookBuilder::default().into_hooks();
let panic = panic.into_panic_hook();
let error = error.into_eyre_hook();
eyre::set_hook(Box::new(move |e| {
let _ = restore_terminal();
error(e)
}))?;
panic::set_hook(Box::new(move |info| {
let _ = restore_terminal();
panic(info);
}));
Ok(())
}
#[instrument]
fn init_terminal() -> Result<Terminal<impl Backend>> {
enable_raw_mode()?;
stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
debug!("terminal initialized");
Ok(terminal)
}
#[instrument]
fn restore_terminal() -> Result<()> {
disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
debug!("terminal restored");
Ok(())
}

View File

@@ -1,30 +0,0 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/constraints.tape`
Output "target/constraint-explorer.gif"
Set Theme "Aardvark Blue"
Set FontSize 18
Set Width 1200
Set Height 950
Hide
Type "cargo run --example=constraint-explorer --features=crossterm"
Enter
Sleep 2s
Show
Set TypingSpeed 2s
Type "1"
Type "2"
Right
Type "4"
Type "5"
Up
Up
Down
Down
Right
Set TypingSpeed 0.5s
Type "++++++++"
Type "--------"
Type "aaa"
Sleep 2s
Type "xxx"
Hide

View File

@@ -1,15 +0,0 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
Output "target/hyperlink.gif"
Set Theme "Aardvark Blue"
Set Width 600
Set Height 150
Hide
Type "cargo run --example=hyperlink --features=crossterm,unstable-widget-ref"
Enter
Sleep 2s
Show
Sleep 1s
Hide
Type "q"

View File

@@ -1,12 +0,0 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/hello_world.tape`
Output "target/minimal.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 200
Hide
Type "cargo run --example=minimal --features=crossterm"
Enter
Sleep 1s
Show
Sleep 5s

View File

@@ -1,12 +0,0 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/barchart.tape`
Output "target/tracing.gif"
Set Theme "Aardvark Blue"
Set Width 1200
Set Height 800
Type "RUST_LOG=trace cargo run --example=tracing" Enter
Sleep 1s
Type @100ms "jjjjq"
Sleep 1s
Type "cat tracing.log" Enter
Sleep 10s

View File

@@ -104,10 +104,7 @@ use std::io;
use strum::{Display, EnumString};
use crate::{
buffer::Cell,
layout::{Rect, Size},
};
use crate::{buffer::Cell, layout::Size, prelude::Rect};
#[cfg(feature = "termion")]
mod termion;

View File

@@ -5,77 +5,77 @@
use std::io::{self, Write};
#[cfg(feature = "underline-color")]
use crate::crossterm::style::SetUnderlineColor;
use crossterm::style::SetUnderlineColor;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
crossterm::{
cursor::{Hide, MoveTo, Show},
event::{
DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags,
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
},
execute, queue,
style::{
Attribute as CAttribute, Attributes as CAttributes, Color as CColor, Colors,
ContentStyle, Print, SetAttribute, SetBackgroundColor, SetColors, SetForegroundColor,
},
terminal::{
disable_raw_mode, enable_raw_mode, Clear, EnterAlternateScreen, LeaveAlternateScreen,
},
terminal::{self, Clear},
},
layout::{Rect, Size},
layout::Size,
prelude::Rect,
style::{Color, Modifier, Style},
};
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
///
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is used
/// to send commands to the terminal. It provides methods for drawing content, manipulating the
/// cursor, and clearing the terminal screen.
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
/// the cursor, and clearing the terminal screen.
///
/// Convenience methods ([`CrosstermBackend::stdout`] and [`CrosstermBackend::stderr`] are provided
/// to create a `CrosstermBackend` with [`std::io::stdout`] or [`std::io::stderr`] as the writer
/// with raw mode enabled and the terminal switched to the alternate screen.
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
///
/// If the default settings are not desired, the `CrosstermBackend` can be configured using the
/// `with_*` methods. These methods return an [`io::Result`] containing self so that they can be
/// chained with other methods. The settings are restored when the `CrosstermBackend` is dropped.
/// - [`CrosstermBackend::with_raw_mode`] enables raw mode for the terminal.
/// - [`CrosstermBackend::with_alternate_screen`] switches to the alternate screen.
/// - [`CrosstermBackend::with_mouse_capture`] enables mouse capture.
/// - [`CrosstermBackend::with_bracketed_paste`] enables bracketed paste.
/// - [`CrosstermBackend::with_focus_change`] enables focus change.
/// - [`CrosstermBackend::with_keyboard_enhancement_flags`] enables keyboard enhancement flags.
///
/// If a backend is configured using the `with_*` methods, the settings are restored when the
/// `CrosstermBackend` is dropped.
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
/// when the application exits). This is not done automatically by the backend because it is
/// possible that the application may want to use the terminal for other purposes (like showing
/// help text) before entering alternate screen mode.
///
/// # Example
///
/// ```rust,no_run
/// use std::io::{stderr, stdout};
///
/// use ratatui::{
/// backend::{Backend, CrosstermBackend},
/// crossterm::event::KeyboardEnhancementFlags,
/// crossterm::{
/// terminal::{
/// disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
/// },
/// ExecutableCommand,
/// },
/// prelude::*,
/// };
///
/// let backend = CrosstermBackend::stdout()?;
/// let mut backend = CrosstermBackend::new(stdout());
/// // or
/// let backend = CrosstermBackend::stderr()?;
/// // or with custom settings
/// let backend = CrosstermBackend::new(std::io::stdout())
/// .with_raw_mode()?
/// .with_alternate_screen()?
/// .with_mouse_capture()?
/// .with_bracketed_paste()?
/// .with_focus_change()?
/// .with_keyboard_enhancement_flags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)?;
/// let backend = CrosstermBackend::new(stderr());
/// let mut terminal = Terminal::new(backend)?;
///
/// enable_raw_mode()?;
/// stdout().execute(EnterAlternateScreen)?;
///
/// terminal.clear()?;
/// terminal.draw(|frame| {
/// // -- snip --
/// })?;
///
/// stdout().execute(LeaveAlternateScreen)?;
/// disable_raw_mode()?;
///
/// # std::io::Result::Ok(())
/// ```
///
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation for
/// more details on raw mode and alternate screen.
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
/// for more details on raw mode and alternate screen.
///
/// [`Write`]: std::io::Write
/// [`Terminal`]: crate::terminal::Terminal
@@ -83,16 +83,9 @@ use crate::{
/// [Crossterm]: https://crates.io/crates/crossterm
/// [Examples]: https://github.com/ratatui-org/ratatui/tree/main/examples/README.md
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[allow(clippy::struct_excessive_bools)]
pub struct CrosstermBackend<W: Write> {
/// The writer used to send commands to the terminal.
writer: W,
restore_raw_mode_on_drop: bool,
restore_alternate_screen_on_drop: bool,
restore_mouse_capture_on_drop: bool,
restore_bracketed_paste_on_drop: bool,
restore_focus_change_on_drop: bool,
restore_keyboard_enhancement_flags_on_drop: bool,
}
impl<W> CrosstermBackend<W>
@@ -101,26 +94,15 @@ where
{
/// Creates a new `CrosstermBackend` with the given writer.
///
/// Applications will typically use [`CrosstermBackend::stdout`] or [`CrosstermBackend::stderr`]
/// to create a `CrosstermBackend` with [`std::io::stdout`] or [`std::io::stderr`] as the
/// writer.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::new(std::io::stdout());
/// # use std::io::stdout;
/// # use ratatui::prelude::*;
/// let backend = CrosstermBackend::new(stdout());
/// ```
pub const fn new(writer: W) -> Self {
Self {
writer,
restore_raw_mode_on_drop: false,
restore_alternate_screen_on_drop: false,
restore_mouse_capture_on_drop: false,
restore_bracketed_paste_on_drop: false,
restore_focus_change_on_drop: false,
restore_keyboard_enhancement_flags_on_drop: false,
}
Self { writer }
}
/// Gets the writer.
@@ -145,223 +127,6 @@ where
}
}
impl CrosstermBackend<io::Stdout> {
/// Creates a new `CrosstermBackend` with `std::io::stdout`, enables raw mode, and switches to
/// the alternate screen.
///
/// Raw mode and alternate screen are restored when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout();
/// ```
pub fn stdout() -> io::Result<Self> {
Self::new(io::stdout())
.with_raw_mode()?
.with_alternate_screen()
}
}
impl CrosstermBackend<io::Stderr> {
/// Creates a new `CrosstermBackend` with `std::io::stderr`, enables raw mode, and switches to
/// the alternate screen.
///
/// Raw mode and alternate screen are restored when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stderr();
/// ```
pub fn stderr() -> io::Result<Self> {
Self::new(io::stderr())
.with_raw_mode()?
.with_alternate_screen()
}
}
impl<W: Write> CrosstermBackend<W> {
/// Enables raw mode for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// Raw mode is restored when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout()?.with_raw_mode()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_raw_mode(mut self) -> io::Result<Self> {
enable_raw_mode()?;
self.restore_raw_mode_on_drop = true;
Ok(self)
}
/// Switches to the alternate screen.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// Alternate screen is restored when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout()?.with_alternate_screen()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_alternate_screen(mut self) -> io::Result<Self> {
execute!(self.writer, EnterAlternateScreen)?;
self.restore_alternate_screen_on_drop = true;
Ok(self)
}
/// Enables mouse capture for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// Mouse capture is disabled when the `CrosstermBackend` is dropped.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout()?.with_mouse_capture()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_mouse_capture(mut self) -> io::Result<Self> {
execute!(self.writer, EnableMouseCapture)?;
self.restore_mouse_capture_on_drop = true;
Ok(self)
}
/// Enables bracketed paste for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout()?.with_bracketed_paste()?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_bracketed_paste(mut self) -> io::Result<Self> {
execute!(self.writer, EnableBracketedPaste)?;
self.restore_bracketed_paste_on_drop = true;
Ok(self)
}
/// Enables focus change for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// let backend = CrosstermBackend::stdout()?.with_focus_change()?;
/// # std::io::Result::Ok(())
pub fn with_focus_change(mut self) -> io::Result<Self> {
execute!(self.writer, EnableFocusChange)?;
self.restore_focus_change_on_drop = true;
Ok(self)
}
/// Enables keyboard enhancement flags for the terminal.
///
/// Returns an [`io::Result`] containing self so that it can be chained with other methods.
///
/// # Example
///
/// ```rust,no_run
/// use ratatui::{backend::CrosstermBackend, crossterm::event::KeyboardEnhancementFlags};
///
/// let backend = CrosstermBackend::stdout()?
/// .with_keyboard_enhancement_flags(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES)?;
/// # std::io::Result::Ok(())
/// ```
pub fn with_keyboard_enhancement_flags(
mut self,
flags: KeyboardEnhancementFlags,
) -> io::Result<Self> {
execute!(self.writer, PushKeyboardEnhancementFlags(flags))?;
self.restore_keyboard_enhancement_flags_on_drop = true;
Ok(self)
}
/// Restores the terminal to its default state.
///
/// This method:
///
/// - Disables raw mode
/// - Leaves the alternate screen
/// - Disables mouse capture
/// - Disables bracketed paste
/// - Disables focus change
/// - Pops keyboard enhancement flags
///
/// This method is an associated method rather than an instance method to make it possible to
/// call without having a `CrosstermBackend` instance. This is often useful in the context of
/// error / panic handling.
///
/// If you have created a `CrosstermBackend` using the `with_*` methods, the settings are
/// restored when the `CrosstermBackend` is dropped, so you do not need to call this method
/// manually.
///
/// # Example
///
/// ```rust,no_run
/// # use ratatui::backend::CrosstermBackend;
/// CrosstermBackend::restore(std::io::stderr())?;
/// # std::io::Result::Ok(())
/// ```
pub fn restore(mut writer: W) -> io::Result<()> {
disable_raw_mode()?;
execute!(
writer,
LeaveAlternateScreen,
DisableMouseCapture,
DisableBracketedPaste,
DisableFocusChange,
PopKeyboardEnhancementFlags
)?;
writer.flush()
}
}
impl<W: Write> Drop for CrosstermBackend<W> {
fn drop(&mut self) {
// note that these are not checked for errors because there is nothing that can be done if
// they fail. The terminal is likely in a bad state, and the application is exiting anyway.
if self.restore_raw_mode_on_drop {
let _ = disable_raw_mode();
}
if self.restore_mouse_capture_on_drop {
let _ = execute!(self.writer, DisableMouseCapture);
}
if self.restore_alternate_screen_on_drop {
let _ = execute!(self.writer, LeaveAlternateScreen);
}
if self.restore_bracketed_paste_on_drop {
let _ = execute!(self.writer, DisableBracketedPaste);
}
if self.restore_focus_change_on_drop {
let _ = execute!(self.writer, DisableFocusChange);
}
if self.restore_keyboard_enhancement_flags_on_drop {
let _ = execute!(self.writer, PopKeyboardEnhancementFlags);
}
let _ = self.writer.flush();
}
}
impl<W> Write for CrosstermBackend<W>
where
W: Write,
@@ -482,7 +247,7 @@ where
}
fn size(&self) -> io::Result<Rect> {
let (width, height) = crossterm::terminal::size()?;
let (width, height) = terminal::size()?;
Ok(Rect::new(0, 0, width, height))
}
@@ -492,7 +257,7 @@ where
rows,
width,
height,
} = crossterm::terminal::window_size()?;
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
@@ -572,6 +337,7 @@ impl ModifierDiff {
where
W: io::Write,
{
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
queue!(w, SetAttribute(CAttribute::NoReverse))?;

View File

@@ -609,18 +609,16 @@ mod tests {
#[test]
fn set_string_zero_width() {
assert_eq!("\u{200B}".width(), 0);
let area = Rect::new(0, 0, 1, 1);
let mut buffer = Buffer::empty(area);
// Leading grapheme with zero width
let s = "\u{200B}a";
let s = "\u{1}a";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
// Trailing grapheme with zero with
let s = "a\u{200B}";
let s = "a\u{1}";
buffer.set_stringn(0, 0, s, 1, Style::default());
assert_eq!(buffer, Buffer::with_lines(["a"]));
}

View File

@@ -67,14 +67,6 @@ impl Cell {
self
}
/// Appends a symbol to the cell.
///
/// This is particularly useful for adding zero-width characters to the cell.
pub(crate) fn append_symbol(&mut self, symbol: &str) -> &mut Self {
self.symbol.push_str(symbol);
self
}
/// Sets the symbol of the cell to a single character.
pub fn set_char(&mut self, ch: char) -> &mut Self {
let mut buf = [0; 4];
@@ -162,128 +154,16 @@ mod tests {
use super::*;
#[test]
fn new() {
let cell = Cell::new("");
assert_eq!(
cell,
Cell {
symbol: CompactString::new_inline(""),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
underline_color: Color::Reset,
modifier: Modifier::empty(),
skip: false,
}
);
}
#[test]
fn empty() {
let cell = Cell::EMPTY;
assert_eq!(cell.symbol(), " ");
}
#[test]
fn set_symbol() {
fn symbol_field() {
let mut cell = Cell::EMPTY;
assert_eq!(cell.symbol(), " ");
cell.set_symbol(""); // Multi-byte character
assert_eq!(cell.symbol(), "");
cell.set_symbol("👨‍👩‍👧‍👦"); // Multiple code units combined with ZWJ
assert_eq!(cell.symbol(), "👨‍👩‍👧‍👦");
}
#[test]
fn append_symbol() {
let mut cell = Cell::EMPTY;
cell.set_symbol(""); // Multi-byte character
cell.append_symbol("\u{200B}"); // zero-width space
assert_eq!(cell.symbol(), "\u{200B}");
}
#[test]
fn set_char() {
let mut cell = Cell::EMPTY;
cell.set_char('あ'); // Multi-byte character
assert_eq!(cell.symbol(), "");
}
#[test]
fn set_fg() {
let mut cell = Cell::EMPTY;
cell.set_fg(Color::Red);
assert_eq!(cell.fg, Color::Red);
}
#[test]
fn set_bg() {
let mut cell = Cell::EMPTY;
cell.set_bg(Color::Red);
assert_eq!(cell.bg, Color::Red);
}
#[test]
fn set_style() {
let mut cell = Cell::EMPTY;
cell.set_style(Style::new().fg(Color::Red).bg(Color::Blue));
assert_eq!(cell.fg, Color::Red);
assert_eq!(cell.bg, Color::Blue);
}
#[test]
fn set_skip() {
let mut cell = Cell::EMPTY;
cell.set_skip(true);
assert!(cell.skip);
}
#[test]
fn reset() {
let mut cell = Cell::EMPTY;
cell.set_symbol("");
cell.set_fg(Color::Red);
cell.set_bg(Color::Blue);
cell.set_skip(true);
cell.reset();
assert_eq!(cell.symbol(), " ");
assert_eq!(cell.fg, Color::Reset);
assert_eq!(cell.bg, Color::Reset);
assert!(!cell.skip);
}
#[test]
fn style() {
let cell = Cell::EMPTY;
assert_eq!(
cell.style(),
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
#[cfg(feature = "underline-color")]
underline_color: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
);
}
#[test]
fn default() {
let cell = Cell::default();
assert_eq!(cell.symbol(), " ");
}
#[test]
fn cell_eq() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_eq!(cell1, cell2);
}
#[test]
fn cell_ne() {
let cell1 = Cell::new("");
let cell2 = Cell::new("");
assert_ne!(cell1, cell2);
// above Cell::EMPTY is put into a mutable variable and is changed then.
// While this looks like it might change the constant, it actually doesnt:
assert_eq!(Cell::EMPTY.symbol(), " ");
}
}

View File

@@ -1881,11 +1881,14 @@ mod tests {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
])
.constraints(
[
Constraint::Percentage(10),
Constraint::Max(5),
Constraint::Min(1),
]
.as_ref(),
)
.split(target);
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());

View File

@@ -1,6 +1,4 @@
#![warn(missing_docs)]
use std::fmt;
use crate::layout::Rect;
/// Position in the terminal
@@ -63,12 +61,6 @@ impl From<Rect> for Position {
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -102,10 +94,4 @@ mod tests {
assert_eq!(position.x, 1);
assert_eq!(position.y, 2);
}
#[test]
fn to_string() {
let position = Position::new(1, 2);
assert_eq!(position.to_string(), "(1, 2)");
}
}

View File

@@ -58,7 +58,7 @@ impl Rect {
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
/// clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
let max_area = u16::MAX;
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);

View File

@@ -1,3 +1,4 @@
use self::layout::Position;
use crate::prelude::*;
/// An iterator over rows within a `Rect`.

View File

@@ -1,6 +1,4 @@
#![warn(missing_docs)]
use std::fmt;
use crate::prelude::*;
/// A simple size struct
@@ -34,12 +32,6 @@ impl From<Rect> for Size {
}
}
impl fmt::Display for Size {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}x{}", self.width, self.height)
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -64,9 +56,4 @@ mod tests {
assert_eq!(size.width, 10);
assert_eq!(size.height, 20);
}
#[test]
fn display() {
assert_eq!(Size::new(10, 20).to_string(), "10x20");
}
}

View File

@@ -2,10 +2,10 @@
//!
//! <div align="center">
//!
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
//! Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
//! Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
//! [![Forum Badge]][Forum]<br>
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
//! [![Matrix Badge]][Matrix]<br>
//!
//! [Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
//! [Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
@@ -44,7 +44,6 @@
//! ## Other documentation
//!
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
//! - [Ratatui Forum][Forum] - a place to ask questions and discuss the library
//! - [API Docs] - the full API documentation for the library on docs.rs.
//! - [Examples] - a collection of examples that demonstrate how to use the library.
//! - [Contributing] - Please read this if you are interested in contributing to the project.
@@ -319,8 +318,6 @@
//! [Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
//! [Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
//! [Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
//! [Forum]: https://forum.ratatui.rs
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square&color=1370D3
// show the feature flags in the generated documentation
@@ -330,24 +327,26 @@
html_favicon_url = "https://raw.githubusercontent.com/ratatui-org/ratatui/main/assets/favicon.ico"
)]
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod text;
pub mod widgets;
#[doc(inline)]
pub use self::terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
pub mod prelude;
/// re-export the `crossterm` crate so that users don't have to add it as a dependency
#[cfg(feature = "crossterm")]
pub use crossterm;
#[doc(inline)]
pub use terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport};
/// re-export the `termion` crate so that users don't have to add it as a dependency
#[cfg(feature = "termion")]
pub use termion;
/// re-export the `termwiz` crate so that users don't have to add it as a dependency
#[cfg(feature = "termwiz")]
pub use termwiz;
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod prelude;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod text;
pub mod widgets;

View File

@@ -27,10 +27,10 @@ pub(crate) use crate::widgets::{StatefulWidgetRef, WidgetRef};
pub use crate::{
backend::{self, Backend},
buffer::{self, Buffer},
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Position, Rect, Size},
style::{self, Color, Modifier, Style, Stylize},
symbols::{self},
terminal::{Frame, Terminal},
layout::{self, Alignment, Constraint, Direction, Layout, Margin, Rect},
style::{self, Color, Modifier, Style, Styled, Stylize},
symbols::{self, Marker},
terminal::{CompletedFrame, Frame, Terminal, TerminalOptions, Viewport},
text::{self, Line, Masked, Span, Text},
widgets::{block::BlockExt, StatefulWidget, Widget},
};

View File

@@ -71,15 +71,14 @@
use std::fmt;
use bitflags::bitflags;
pub use color::{Color, ParseColorError};
pub use stylize::{Styled, Stylize};
mod color;
pub mod palette;
#[cfg(feature = "palette")]
mod palette_conversion;
mod stylize;
pub use color::{Color, ParseColorError};
pub use stylize::{Styled, Stylize};
pub mod palette;
bitflags! {
/// Modifier changes the way a piece of text is displayed.
///

View File

@@ -1,83 +0,0 @@
//! Conversions from colors in the `palette` crate to [`Color`].
use ::palette::{
bool_mask::LazySelect,
num::{Arithmetics, MulSub, PartialCmp, Powf, Real},
LinSrgb,
};
use palette::{stimulus::IntoStimulus, Srgb};
use super::Color;
/// Convert an [`palette::Srgb`] color to a [`Color`].
///
/// # Examples
///
/// ```
/// use palette::Srgb;
/// use ratatui::style::Color;
///
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
/// ```
impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
fn from(color: Srgb<T>) -> Self {
let (red, green, blue) = color.into_format().into_components();
Self::Rgb(red, green, blue)
}
}
/// Convert a [`palette::LinSrgb`] color to a [`Color`].
///
/// Note: this conversion only works for floating point linear sRGB colors. If you have a linear
/// sRGB color in another format, you need to convert it to floating point first.
///
/// # Examples
///
/// ```
/// use palette::LinSrgb;
/// use ratatui::style::Color;
///
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
/// assert_eq!(color, Color::Rgb(255, 0, 0));
/// ```
impl<T: IntoStimulus<u8>> From<LinSrgb<T>> for Color
where
T: Real + Powf + MulSub + Arithmetics + PartialCmp + Clone,
T::Mask: LazySelect<T>,
{
fn from(color: LinSrgb<T>) -> Self {
let srgb_color = Srgb::<T>::from_linear(color);
Self::from(srgb_color)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(Srgb::new(255u8, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(65535u16, 0, 0)), RED);
assert_eq!(Color::from(Srgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(
Color::from(Srgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(128, 128, 128)
);
}
#[test]
fn from_lin_srgb() {
const RED: Color = Color::Rgb(255, 0, 0);
assert_eq!(Color::from(LinSrgb::new(1.0f32, 0.0, 0.0)), RED);
assert_eq!(Color::from(LinSrgb::new(1.0f64, 0.0, 0.0)), RED);
assert_eq!(
Color::from(LinSrgb::new(0.5f32, 0.5, 0.5)),
Color::Rgb(188, 188, 188)
);
}
}

View File

@@ -1,8 +1,5 @@
use strum::{Display, EnumString};
pub mod border;
pub mod line;
pub mod block {
pub const FULL: &str = "";
pub const SEVEN_EIGHTHS: &str = "";
@@ -117,6 +114,364 @@ pub mod bar {
};
}
pub mod line {
pub const VERTICAL: &str = "";
pub const DOUBLE_VERTICAL: &str = "";
pub const THICK_VERTICAL: &str = "";
pub const HORIZONTAL: &str = "";
pub const DOUBLE_HORIZONTAL: &str = "";
pub const THICK_HORIZONTAL: &str = "";
pub const TOP_RIGHT: &str = "";
pub const ROUNDED_TOP_RIGHT: &str = "";
pub const DOUBLE_TOP_RIGHT: &str = "";
pub const THICK_TOP_RIGHT: &str = "";
pub const TOP_LEFT: &str = "";
pub const ROUNDED_TOP_LEFT: &str = "";
pub const DOUBLE_TOP_LEFT: &str = "";
pub const THICK_TOP_LEFT: &str = "";
pub const BOTTOM_RIGHT: &str = "";
pub const ROUNDED_BOTTOM_RIGHT: &str = "";
pub const DOUBLE_BOTTOM_RIGHT: &str = "";
pub const THICK_BOTTOM_RIGHT: &str = "";
pub const BOTTOM_LEFT: &str = "";
pub const ROUNDED_BOTTOM_LEFT: &str = "";
pub const DOUBLE_BOTTOM_LEFT: &str = "";
pub const THICK_BOTTOM_LEFT: &str = "";
pub const VERTICAL_LEFT: &str = "";
pub const DOUBLE_VERTICAL_LEFT: &str = "";
pub const THICK_VERTICAL_LEFT: &str = "";
pub const VERTICAL_RIGHT: &str = "";
pub const DOUBLE_VERTICAL_RIGHT: &str = "";
pub const THICK_VERTICAL_RIGHT: &str = "";
pub const HORIZONTAL_DOWN: &str = "";
pub const DOUBLE_HORIZONTAL_DOWN: &str = "";
pub const THICK_HORIZONTAL_DOWN: &str = "";
pub const HORIZONTAL_UP: &str = "";
pub const DOUBLE_HORIZONTAL_UP: &str = "";
pub const THICK_HORIZONTAL_UP: &str = "";
pub const CROSS: &str = "";
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
pub top_right: &'static str,
pub top_left: &'static str,
pub bottom_right: &'static str,
pub bottom_left: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_down: &'static str,
pub horizontal_up: &'static str,
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
top_right: TOP_RIGHT,
top_left: TOP_LEFT,
bottom_right: BOTTOM_RIGHT,
bottom_left: BOTTOM_LEFT,
vertical_left: VERTICAL_LEFT,
vertical_right: VERTICAL_RIGHT,
horizontal_down: HORIZONTAL_DOWN,
horizontal_up: HORIZONTAL_UP,
cross: CROSS,
};
pub const ROUNDED: Set = Set {
top_right: ROUNDED_TOP_RIGHT,
top_left: ROUNDED_TOP_LEFT,
bottom_right: ROUNDED_BOTTOM_RIGHT,
bottom_left: ROUNDED_BOTTOM_LEFT,
..NORMAL
};
pub const DOUBLE: Set = Set {
vertical: DOUBLE_VERTICAL,
horizontal: DOUBLE_HORIZONTAL,
top_right: DOUBLE_TOP_RIGHT,
top_left: DOUBLE_TOP_LEFT,
bottom_right: DOUBLE_BOTTOM_RIGHT,
bottom_left: DOUBLE_BOTTOM_LEFT,
vertical_left: DOUBLE_VERTICAL_LEFT,
vertical_right: DOUBLE_VERTICAL_RIGHT,
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
horizontal_up: DOUBLE_HORIZONTAL_UP,
cross: DOUBLE_CROSS,
};
pub const THICK: Set = Set {
vertical: THICK_VERTICAL,
horizontal: THICK_HORIZONTAL,
top_right: THICK_TOP_RIGHT,
top_left: THICK_TOP_LEFT,
bottom_right: THICK_BOTTOM_RIGHT,
bottom_left: THICK_BOTTOM_LEFT,
vertical_left: THICK_VERTICAL_LEFT,
vertical_right: THICK_VERTICAL_RIGHT,
horizontal_down: THICK_HORIZONTAL_DOWN,
horizontal_up: THICK_HORIZONTAL_UP,
cross: THICK_CROSS,
};
}
pub mod border {
use super::line;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_top: &'static str,
pub horizontal_bottom: &'static str,
}
impl Default for Set {
fn default() -> Self {
PLAIN
}
}
/// Border Set with a single line width
///
/// ```text
/// ┌─────┐
/// │xxxxx│
/// │xxxxx│
/// └─────┘
pub const PLAIN: Set = Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: line::NORMAL.vertical,
vertical_right: line::NORMAL.vertical,
horizontal_top: line::NORMAL.horizontal,
horizontal_bottom: line::NORMAL.horizontal,
};
/// Border Set with a single line width and rounded corners
///
/// ```text
/// ╭─────╮
/// │xxxxx│
/// │xxxxx│
/// ╰─────╯
pub const ROUNDED: Set = Set {
top_left: line::ROUNDED.top_left,
top_right: line::ROUNDED.top_right,
bottom_left: line::ROUNDED.bottom_left,
bottom_right: line::ROUNDED.bottom_right,
vertical_left: line::ROUNDED.vertical,
vertical_right: line::ROUNDED.vertical,
horizontal_top: line::ROUNDED.horizontal,
horizontal_bottom: line::ROUNDED.horizontal,
};
/// Border Set with a double line width
///
/// ```text
/// ╔═════╗
/// ║xxxxx║
/// ║xxxxx║
/// ╚═════╝
pub const DOUBLE: Set = Set {
top_left: line::DOUBLE.top_left,
top_right: line::DOUBLE.top_right,
bottom_left: line::DOUBLE.bottom_left,
bottom_right: line::DOUBLE.bottom_right,
vertical_left: line::DOUBLE.vertical,
vertical_right: line::DOUBLE.vertical,
horizontal_top: line::DOUBLE.horizontal,
horizontal_bottom: line::DOUBLE.horizontal,
};
/// Border Set with a thick line width
///
/// ```text
/// ┏━━━━━┓
/// ┃xxxxx┃
/// ┃xxxxx┃
/// ┗━━━━━┛
pub const THICK: Set = Set {
top_left: line::THICK.top_left,
top_right: line::THICK.top_right,
bottom_left: line::THICK.bottom_left,
bottom_right: line::THICK.bottom_right,
vertical_left: line::THICK.vertical,
vertical_right: line::THICK.vertical,
horizontal_top: line::THICK.horizontal,
horizontal_bottom: line::THICK.horizontal,
};
pub const QUADRANT_TOP_LEFT: &str = "";
pub const QUADRANT_TOP_RIGHT: &str = "";
pub const QUADRANT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_HALF: &str = "";
pub const QUADRANT_BOTTOM_HALF: &str = "";
pub const QUADRANT_LEFT_HALF: &str = "";
pub const QUADRANT_RIGHT_HALF: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BLOCK: &str = "";
/// Quadrant used for setting a border outside a block by one half cell "pixel".
///
/// ```text
/// ▛▀▀▀▀▀▜
/// ▌xxxxx▐
/// ▌xxxxx▐
/// ▙▄▄▄▄▄▟
/// ```
pub const QUADRANT_OUTSIDE: Set = Set {
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
vertical_left: QUADRANT_LEFT_HALF,
vertical_right: QUADRANT_RIGHT_HALF,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Quadrant used for setting a border inside a block by one half cell "pixel".
///
/// ```text
/// ▗▄▄▄▄▄▖
/// ▐xxxxx▌
/// ▐xxxxx▌
/// ▝▀▀▀▀▀▘
/// ```
pub const QUADRANT_INSIDE: Set = Set {
top_right: QUADRANT_BOTTOM_LEFT,
top_left: QUADRANT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_LEFT,
bottom_left: QUADRANT_TOP_RIGHT,
vertical_left: QUADRANT_RIGHT_HALF,
vertical_right: QUADRANT_LEFT_HALF,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
pub const ONE_EIGHTH_TOP_EIGHT: &str = "";
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "";
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "";
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "";
/// Wide border set based on McGugan box technique
///
/// ```text
/// ▁▁▁▁▁▁▁
/// ▏xxxxx▕
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
bottom_right: ONE_EIGHTH_TOP_EIGHT,
bottom_left: ONE_EIGHTH_TOP_EIGHT,
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
};
/// Tall border set based on McGugan box technique
///
/// ```text
/// ▕▔▔▏
/// ▕xx▏
/// ▕xx▏
/// ▕▁▁▏
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
};
/// Wide proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using half blocks for top and bottom, and full
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▄▄▄▄
/// █xx█
/// █xx█
/// ▀▀▀▀
/// ```
pub const PROPORTIONAL_WIDE: Set = Set {
top_right: QUADRANT_BOTTOM_HALF,
top_left: QUADRANT_BOTTOM_HALF,
bottom_right: QUADRANT_TOP_HALF,
bottom_left: QUADRANT_TOP_HALF,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
/// Tall proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using full blocks for all sides, except for the top and bottom,
/// which use half blocks to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▕█▀▀█
/// ▕█xx█
/// ▕█xx█
/// ▕█▄▄█
/// ```
pub const PROPORTIONAL_TALL: Set = Set {
top_right: QUADRANT_BLOCK,
top_left: QUADRANT_BLOCK,
bottom_right: QUADRANT_BLOCK,
bottom_left: QUADRANT_BLOCK,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
}
pub const DOT: &str = "";
pub mod braille {

View File

@@ -1,504 +0,0 @@
use super::{block, line};
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub top_left: &'static str,
pub top_right: &'static str,
pub bottom_left: &'static str,
pub bottom_right: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_top: &'static str,
pub horizontal_bottom: &'static str,
}
impl Default for Set {
fn default() -> Self {
PLAIN
}
}
/// Border Set with a single line width
///
/// ```text
/// ┌─────┐
/// │xxxxx│
/// │xxxxx│
/// └─────┘
pub const PLAIN: Set = Set {
top_left: line::NORMAL.top_left,
top_right: line::NORMAL.top_right,
bottom_left: line::NORMAL.bottom_left,
bottom_right: line::NORMAL.bottom_right,
vertical_left: line::NORMAL.vertical,
vertical_right: line::NORMAL.vertical,
horizontal_top: line::NORMAL.horizontal,
horizontal_bottom: line::NORMAL.horizontal,
};
/// Border Set with a single line width and rounded corners
///
/// ```text
/// ╭─────╮
/// │xxxxx│
/// │xxxxx│
/// ╰─────╯
pub const ROUNDED: Set = Set {
top_left: line::ROUNDED.top_left,
top_right: line::ROUNDED.top_right,
bottom_left: line::ROUNDED.bottom_left,
bottom_right: line::ROUNDED.bottom_right,
vertical_left: line::ROUNDED.vertical,
vertical_right: line::ROUNDED.vertical,
horizontal_top: line::ROUNDED.horizontal,
horizontal_bottom: line::ROUNDED.horizontal,
};
/// Border Set with a double line width
///
/// ```text
/// ╔═════╗
/// ║xxxxx║
/// ║xxxxx║
/// ╚═════╝
pub const DOUBLE: Set = Set {
top_left: line::DOUBLE.top_left,
top_right: line::DOUBLE.top_right,
bottom_left: line::DOUBLE.bottom_left,
bottom_right: line::DOUBLE.bottom_right,
vertical_left: line::DOUBLE.vertical,
vertical_right: line::DOUBLE.vertical,
horizontal_top: line::DOUBLE.horizontal,
horizontal_bottom: line::DOUBLE.horizontal,
};
/// Border Set with a thick line width
///
/// ```text
/// ┏━━━━━┓
/// ┃xxxxx┃
/// ┃xxxxx┃
/// ┗━━━━━┛
pub const THICK: Set = Set {
top_left: line::THICK.top_left,
top_right: line::THICK.top_right,
bottom_left: line::THICK.bottom_left,
bottom_right: line::THICK.bottom_right,
vertical_left: line::THICK.vertical,
vertical_right: line::THICK.vertical,
horizontal_top: line::THICK.horizontal,
horizontal_bottom: line::THICK.horizontal,
};
pub const QUADRANT_TOP_LEFT: &str = "";
pub const QUADRANT_TOP_RIGHT: &str = "";
pub const QUADRANT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_HALF: &str = "";
pub const QUADRANT_BOTTOM_HALF: &str = "";
pub const QUADRANT_LEFT_HALF: &str = "";
pub const QUADRANT_RIGHT_HALF: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_LEFT_BOTTOM_RIGHT: &str = "";
pub const QUADRANT_TOP_RIGHT_BOTTOM_LEFT: &str = "";
pub const QUADRANT_BLOCK: &str = "";
/// Quadrant used for setting a border outside a block by one half cell "pixel".
///
/// ```text
/// ▛▀▀▀▀▀▜
/// ▌xxxxx▐
/// ▌xxxxx▐
/// ▙▄▄▄▄▄▟
/// ```
pub const QUADRANT_OUTSIDE: Set = Set {
top_left: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_LEFT,
top_right: QUADRANT_TOP_LEFT_TOP_RIGHT_BOTTOM_RIGHT,
bottom_left: QUADRANT_TOP_LEFT_BOTTOM_LEFT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_RIGHT_BOTTOM_LEFT_BOTTOM_RIGHT,
vertical_left: QUADRANT_LEFT_HALF,
vertical_right: QUADRANT_RIGHT_HALF,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Quadrant used for setting a border inside a block by one half cell "pixel".
///
/// ```text
/// ▗▄▄▄▄▄▖
/// ▐xxxxx▌
/// ▐xxxxx▌
/// ▝▀▀▀▀▀▘
/// ```
pub const QUADRANT_INSIDE: Set = Set {
top_right: QUADRANT_BOTTOM_LEFT,
top_left: QUADRANT_BOTTOM_RIGHT,
bottom_right: QUADRANT_TOP_LEFT,
bottom_left: QUADRANT_TOP_RIGHT,
vertical_left: QUADRANT_RIGHT_HALF,
vertical_right: QUADRANT_LEFT_HALF,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
pub const ONE_EIGHTH_TOP_EIGHT: &str = "";
pub const ONE_EIGHTH_BOTTOM_EIGHT: &str = "";
pub const ONE_EIGHTH_LEFT_EIGHT: &str = "";
pub const ONE_EIGHTH_RIGHT_EIGHT: &str = "";
/// Wide border set based on McGugan box technique
///
/// ```text
/// ▁▁▁▁▁▁▁
/// ▏xxxxx▕
/// ▏xxxxx▕
/// ▔▔▔▔▔▔▔
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_WIDE: Set = Set {
top_right: ONE_EIGHTH_BOTTOM_EIGHT,
top_left: ONE_EIGHTH_BOTTOM_EIGHT,
bottom_right: ONE_EIGHTH_TOP_EIGHT,
bottom_left: ONE_EIGHTH_TOP_EIGHT,
vertical_left: ONE_EIGHTH_LEFT_EIGHT,
vertical_right: ONE_EIGHTH_RIGHT_EIGHT,
horizontal_top: ONE_EIGHTH_BOTTOM_EIGHT,
horizontal_bottom: ONE_EIGHTH_TOP_EIGHT,
};
/// Tall border set based on McGugan box technique
///
/// ```text
/// ▕▔▔▏
/// ▕xx▏
/// ▕xx▏
/// ▕▁▁▏
/// ```
#[allow(clippy::doc_markdown)]
pub const ONE_EIGHTH_TALL: Set = Set {
top_right: ONE_EIGHTH_LEFT_EIGHT,
top_left: ONE_EIGHTH_RIGHT_EIGHT,
bottom_right: ONE_EIGHTH_LEFT_EIGHT,
bottom_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_left: ONE_EIGHTH_RIGHT_EIGHT,
vertical_right: ONE_EIGHTH_LEFT_EIGHT,
horizontal_top: ONE_EIGHTH_TOP_EIGHT,
horizontal_bottom: ONE_EIGHTH_BOTTOM_EIGHT,
};
/// Wide proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using half blocks for top and bottom, and full
/// blocks for right and left sides to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▄▄▄▄
/// █xx█
/// █xx█
/// ▀▀▀▀
/// ```
pub const PROPORTIONAL_WIDE: Set = Set {
top_right: QUADRANT_BOTTOM_HALF,
top_left: QUADRANT_BOTTOM_HALF,
bottom_right: QUADRANT_TOP_HALF,
bottom_left: QUADRANT_TOP_HALF,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_BOTTOM_HALF,
horizontal_bottom: QUADRANT_TOP_HALF,
};
/// Tall proportional (visually equal width and height) border with using set of quadrants.
///
/// The border is created by using full blocks for all sides, except for the top and bottom,
/// which use half blocks to make horizontal and vertical borders seem equal.
///
/// ```text
/// ▕█▀▀█
/// ▕█xx█
/// ▕█xx█
/// ▕█▄▄█
/// ```
pub const PROPORTIONAL_TALL: Set = Set {
top_right: QUADRANT_BLOCK,
top_left: QUADRANT_BLOCK,
bottom_right: QUADRANT_BLOCK,
bottom_left: QUADRANT_BLOCK,
vertical_left: QUADRANT_BLOCK,
vertical_right: QUADRANT_BLOCK,
horizontal_top: QUADRANT_TOP_HALF,
horizontal_bottom: QUADRANT_BOTTOM_HALF,
};
/// Solid border set
///
/// The border is created by using full blocks for all sides.
///
/// ```text
/// ████
/// █xx█
/// █xx█
/// ████
pub const FULL: Set = Set {
top_left: block::FULL,
top_right: block::FULL,
bottom_left: block::FULL,
bottom_right: block::FULL,
vertical_left: block::FULL,
vertical_right: block::FULL,
horizontal_top: block::FULL,
horizontal_bottom: block::FULL,
};
/// Empty border set
///
/// The border is created by using empty strings for all sides.
///
/// This is useful for ensuring that the border style is applied to a border on a block with a title
/// without actually drawing a border.
///
/// ░ Example
///
/// `░` represents the content in the area not covered by the border to make it easier to see the
/// blank symbols.
///
/// ```text
/// ░░░░░░░░
/// ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░ ░░
/// ░░ ░░
/// ░░░░░░░░
/// ```
pub const EMPTY: Set = Set {
top_left: " ",
top_right: " ",
bottom_left: " ",
bottom_right: " ",
vertical_left: " ",
vertical_right: " ",
horizontal_top: " ",
horizontal_bottom: " ",
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), PLAIN);
}
/// A helper function to render a border set to a string.
///
/// '░' (U+2591 Light Shade) is used as a placeholder for empty space to make it easier to see
/// the size of the border symbols.
fn render(set: Set) -> String {
formatdoc!(
"░░░░░░
░{}{}{}{}░
░{}░░{}░
░{}░░{}░
░{}{}{}{}░
░░░░░░",
set.top_left,
set.horizontal_top,
set.horizontal_top,
set.top_right,
set.vertical_left,
set.vertical_right,
set.vertical_left,
set.vertical_right,
set.bottom_left,
set.horizontal_bottom,
set.horizontal_bottom,
set.bottom_right
)
}
#[test]
fn plain() {
assert_eq!(
render(PLAIN),
indoc!(
"░░░░░░
░┌──┐░
░│░░│░
░│░░│░
░└──┘░
░░░░░░"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"░░░░░░
░╭──╮░
░│░░│░
░│░░│░
░╰──╯░
░░░░░░"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"░░░░░░
░╔══╗░
░║░░║░
░║░░║░
░╚══╝░
░░░░░░"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"░░░░░░
░┏━━┓░
░┃░░┃░
░┃░░┃░
░┗━━┛░
░░░░░░"
)
);
}
#[test]
fn quadrant_outside() {
assert_eq!(
render(QUADRANT_OUTSIDE),
indoc!(
"░░░░░░
░▛▀▀▜░
░▌░░▐░
░▌░░▐░
░▙▄▄▟░
░░░░░░"
)
);
}
#[test]
fn quadrant_inside() {
assert_eq!(
render(QUADRANT_INSIDE),
indoc!(
"░░░░░░
░▗▄▄▖░
░▐░░▌░
░▐░░▌░
░▝▀▀▘░
░░░░░░"
)
);
}
#[test]
fn one_eighth_wide() {
assert_eq!(
render(ONE_EIGHTH_WIDE),
indoc!(
"░░░░░░
░▁▁▁▁░
░▏░░▕░
░▏░░▕░
░▔▔▔▔░
░░░░░░"
)
);
}
#[test]
fn one_eighth_tall() {
assert_eq!(
render(ONE_EIGHTH_TALL),
indoc!(
"░░░░░░
░▕▔▔▏░
░▕░░▏░
░▕░░▏░
░▕▁▁▏░
░░░░░░"
)
);
}
#[test]
fn proportional_wide() {
assert_eq!(
render(PROPORTIONAL_WIDE),
indoc!(
"░░░░░░
░▄▄▄▄░
░█░░█░
░█░░█░
░▀▀▀▀░
░░░░░░"
)
);
}
#[test]
fn proportional_tall() {
assert_eq!(
render(PROPORTIONAL_TALL),
indoc!(
"░░░░░░
░█▀▀█░
░█░░█░
░█░░█░
░█▄▄█░
░░░░░░"
)
);
}
#[test]
fn full() {
assert_eq!(
render(FULL),
indoc!(
"░░░░░░
░████░
░█░░█░
░█░░█░
░████░
░░░░░░"
)
);
}
#[test]
fn empty() {
assert_eq!(
render(EMPTY),
indoc!(
"░░░░░░
░ ░
░ ░░ ░
░ ░░ ░
░ ░
░░░░░░"
)
);
}
}

View File

@@ -1,208 +0,0 @@
pub const VERTICAL: &str = "";
pub const DOUBLE_VERTICAL: &str = "";
pub const THICK_VERTICAL: &str = "";
pub const HORIZONTAL: &str = "";
pub const DOUBLE_HORIZONTAL: &str = "";
pub const THICK_HORIZONTAL: &str = "";
pub const TOP_RIGHT: &str = "";
pub const ROUNDED_TOP_RIGHT: &str = "";
pub const DOUBLE_TOP_RIGHT: &str = "";
pub const THICK_TOP_RIGHT: &str = "";
pub const TOP_LEFT: &str = "";
pub const ROUNDED_TOP_LEFT: &str = "";
pub const DOUBLE_TOP_LEFT: &str = "";
pub const THICK_TOP_LEFT: &str = "";
pub const BOTTOM_RIGHT: &str = "";
pub const ROUNDED_BOTTOM_RIGHT: &str = "";
pub const DOUBLE_BOTTOM_RIGHT: &str = "";
pub const THICK_BOTTOM_RIGHT: &str = "";
pub const BOTTOM_LEFT: &str = "";
pub const ROUNDED_BOTTOM_LEFT: &str = "";
pub const DOUBLE_BOTTOM_LEFT: &str = "";
pub const THICK_BOTTOM_LEFT: &str = "";
pub const VERTICAL_LEFT: &str = "";
pub const DOUBLE_VERTICAL_LEFT: &str = "";
pub const THICK_VERTICAL_LEFT: &str = "";
pub const VERTICAL_RIGHT: &str = "";
pub const DOUBLE_VERTICAL_RIGHT: &str = "";
pub const THICK_VERTICAL_RIGHT: &str = "";
pub const HORIZONTAL_DOWN: &str = "";
pub const DOUBLE_HORIZONTAL_DOWN: &str = "";
pub const THICK_HORIZONTAL_DOWN: &str = "";
pub const HORIZONTAL_UP: &str = "";
pub const DOUBLE_HORIZONTAL_UP: &str = "";
pub const THICK_HORIZONTAL_UP: &str = "";
pub const CROSS: &str = "";
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
pub top_right: &'static str,
pub top_left: &'static str,
pub bottom_right: &'static str,
pub bottom_left: &'static str,
pub vertical_left: &'static str,
pub vertical_right: &'static str,
pub horizontal_down: &'static str,
pub horizontal_up: &'static str,
pub cross: &'static str,
}
impl Default for Set {
fn default() -> Self {
NORMAL
}
}
pub const NORMAL: Set = Set {
vertical: VERTICAL,
horizontal: HORIZONTAL,
top_right: TOP_RIGHT,
top_left: TOP_LEFT,
bottom_right: BOTTOM_RIGHT,
bottom_left: BOTTOM_LEFT,
vertical_left: VERTICAL_LEFT,
vertical_right: VERTICAL_RIGHT,
horizontal_down: HORIZONTAL_DOWN,
horizontal_up: HORIZONTAL_UP,
cross: CROSS,
};
pub const ROUNDED: Set = Set {
top_right: ROUNDED_TOP_RIGHT,
top_left: ROUNDED_TOP_LEFT,
bottom_right: ROUNDED_BOTTOM_RIGHT,
bottom_left: ROUNDED_BOTTOM_LEFT,
..NORMAL
};
pub const DOUBLE: Set = Set {
vertical: DOUBLE_VERTICAL,
horizontal: DOUBLE_HORIZONTAL,
top_right: DOUBLE_TOP_RIGHT,
top_left: DOUBLE_TOP_LEFT,
bottom_right: DOUBLE_BOTTOM_RIGHT,
bottom_left: DOUBLE_BOTTOM_LEFT,
vertical_left: DOUBLE_VERTICAL_LEFT,
vertical_right: DOUBLE_VERTICAL_RIGHT,
horizontal_down: DOUBLE_HORIZONTAL_DOWN,
horizontal_up: DOUBLE_HORIZONTAL_UP,
cross: DOUBLE_CROSS,
};
pub const THICK: Set = Set {
vertical: THICK_VERTICAL,
horizontal: THICK_HORIZONTAL,
top_right: THICK_TOP_RIGHT,
top_left: THICK_TOP_LEFT,
bottom_right: THICK_BOTTOM_RIGHT,
bottom_left: THICK_BOTTOM_LEFT,
vertical_left: THICK_VERTICAL_LEFT,
vertical_right: THICK_VERTICAL_RIGHT,
horizontal_down: THICK_HORIZONTAL_DOWN,
horizontal_up: THICK_HORIZONTAL_UP,
cross: THICK_CROSS,
};
#[cfg(test)]
mod tests {
use indoc::{formatdoc, indoc};
use super::*;
#[test]
fn default() {
assert_eq!(Set::default(), NORMAL);
}
/// A helper function to render a set of symbols.
fn render(set: Set) -> String {
formatdoc!(
"{}{}{}{}
{}{}{}{}
{}{}{}{}
{}{}{}{}",
set.top_left,
set.horizontal,
set.horizontal_down,
set.top_right,
set.vertical,
" ",
set.vertical,
set.vertical,
set.vertical_right,
set.horizontal,
set.cross,
set.vertical_left,
set.bottom_left,
set.horizontal,
set.horizontal_up,
set.bottom_right
)
}
#[test]
fn normal() {
assert_eq!(
render(NORMAL),
indoc!(
"┌─┬┐
│ ││
├─┼┤
└─┴┘"
)
);
}
#[test]
fn rounded() {
assert_eq!(
render(ROUNDED),
indoc!(
"╭─┬╮
│ ││
├─┼┤
╰─┴╯"
)
);
}
#[test]
fn double() {
assert_eq!(
render(DOUBLE),
indoc!(
"╔═╦╗
║ ║║
╠═╬╣
╚═╩╝"
)
);
}
#[test]
fn thick() {
assert_eq!(
render(THICK),
indoc!(
"┏━┳┓
┃ ┃┃
┣━╋┫
┗━┻┛"
)
);
}
}

View File

@@ -1,6 +1,6 @@
use std::io;
use crate::{backend::ClearType, prelude::*, CompletedFrame, TerminalOptions, Viewport};
use crate::{backend::ClearType, prelude::*};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
///
@@ -126,7 +126,7 @@ where
///
/// ```rust
/// # use std::io::stdout;
/// # use ratatui::{prelude::*, backend::TestBackend, terminal::{Viewport, TerminalOptions}};
/// # use ratatui::{prelude::*, backend::TestBackend};
/// let backend = CrosstermBackend::new(stdout());
/// let viewport = Viewport::Fixed(Rect::new(0, 0, 10, 10));
/// let terminal = Terminal::with_options(backend, TerminalOptions { viewport })?;

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, style::Styled};
use crate::prelude::*;
/// A grapheme associated to a style.
/// Note that, although `StyledGrapheme` is the smallest divisible unit of text,

View File

@@ -4,7 +4,8 @@ use std::{borrow::Cow, fmt};
use unicode_truncate::UnicodeTruncateStr;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
use super::StyledGrapheme;
use crate::prelude::*;
/// A line of text, consisting of one or more [`Span`]s.
///
@@ -12,9 +13,6 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// text. When a [`Line`] is rendered, it is rendered as a single line of text, with each [`Span`]
/// being rendered in order (left to right).
///
/// Any newlines in the content are removed when creating a [`Line`] using the constructor or
/// conversion methods.
///
/// # Constructor Methods
///
/// - [`Line::default`] creates a line with empty content and the default style.
@@ -505,13 +503,13 @@ impl<'a> IntoIterator for &'a mut Line<'a> {
impl<'a> From<String> for Line<'a> {
fn from(s: String) -> Self {
Self::raw(s)
Self::from(vec![Span::from(s)])
}
}
impl<'a> From<&'a str> for Line<'a> {
fn from(s: &'a str) -> Self {
Self::raw(s)
Self::from(vec![Span::from(s)])
}
}
@@ -806,22 +804,14 @@ mod tests {
fn from_string() {
let s = String::from("Hello, world!");
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
let s = String::from("Hello\nworld!");
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
fn from_str() {
let s = "Hello, world!";
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello, world!")]);
let s = "Hello\nworld!";
let line = Line::from(s);
assert_eq!(line.spans, vec![Span::from("Hello"), Span::from("world!")]);
assert_eq!(vec![Span::from("Hello, world!")], line.spans);
}
#[test]
@@ -831,7 +821,7 @@ mod tests {
Span::styled(" world!", Style::default().fg(Color::Green)),
];
let line = Line::from(spans.clone());
assert_eq!(line.spans, spans);
assert_eq!(spans, line.spans);
}
#[test]
@@ -864,7 +854,7 @@ mod tests {
fn from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));
let line = Line::from(span.clone());
assert_eq!(line.spans, vec![span],);
assert_eq!(vec![span], line.spans);
}
#[test]
@@ -874,7 +864,7 @@ mod tests {
Span::styled(" world!", Style::default().fg(Color::Green)),
]);
let s: String = line.into();
assert_eq!(s, "Hello, world!");
assert_eq!("Hello, world!", s);
}
#[test]

View File

@@ -3,7 +3,8 @@ use std::{borrow::Cow, fmt};
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
use super::StyledGrapheme;
use crate::prelude::*;
/// Represents a part of a line that is contiguous and where all characters share the same style.
///
@@ -362,46 +363,34 @@ impl Widget for Span<'_> {
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let Rect { mut x, y, .. } = area.intersection(buf.area);
for (i, grapheme) in self.styled_graphemes(Style::default()).enumerate() {
let symbol_width = grapheme.symbol.width();
let next_x = x.saturating_add(symbol_width as u16);
if next_x > area.intersection(buf.area).right() {
let area = area.intersection(buf.area);
let Rect {
x: mut current_x,
y,
width,
..
} = area;
let max_x = Ord::min(current_x.saturating_add(width), buf.area.right());
for g in self.styled_graphemes(Style::default()) {
let symbol_width = g.symbol.width();
let next_x = current_x.saturating_add(symbol_width as u16);
if next_x > max_x {
break;
}
if i == 0 {
// the first grapheme is always set on the cell
buf.get_mut(x, y)
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if x == area.x {
// there is one or more zero-width graphemes in the first cell, so the first cell
// must be appended to.
buf.get_mut(x, y)
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else if symbol_width == 0 {
// append zero-width graphemes to the previous cell
buf.get_mut(x - 1, y)
.append_symbol(grapheme.symbol)
.set_style(grapheme.style);
} else {
// just a normal grapheme (not first, not zero-width, not overflowing the area)
buf.get_mut(x, y)
.set_symbol(grapheme.symbol)
.set_style(grapheme.style);
}
buf.get_mut(current_x, y)
.set_symbol(g.symbol)
.set_style(g.style);
// multi-width graphemes must clear the cells of characters that are hidden by the
// grapheme, otherwise the hidden characters will be re-rendered if the grapheme is
// overwritten.
for x_hidden in (x + 1)..next_x {
for i in (current_x + 1)..next_x {
buf.get_mut(i, y).reset();
// it may seem odd that the style of the hidden cells are not set to the style of
// the grapheme, but this is how the existing buffer.set_span() method works.
buf.get_mut(x_hidden, y).reset();
// buf.get_mut(i, y).set_style(g.style);
}
x = next_x;
current_x = next_x;
}
}
}
@@ -414,7 +403,6 @@ impl fmt::Display for Span<'_> {
#[cfg(test)]
mod tests {
use buffer::Cell;
use rstest::fixture;
use super::*;
@@ -676,72 +664,5 @@ mod tests {
])]);
assert_eq!(buf, expected);
}
#[test]
fn render_first_zero_width() {
let span = Span::raw("\u{200B}abc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("\u{200B}a"), Cell::new("b"), Cell::new("c"),]
);
}
#[test]
fn render_second_zero_width() {
let span = Span::raw("a\u{200B}bc");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a\u{200B}"), Cell::new("b"), Cell::new("c")]
);
}
#[test]
fn render_middle_zero_width() {
let span = Span::raw("ab\u{200B}c");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b\u{200B}"), Cell::new("c")]
);
}
#[test]
fn render_last_zero_width() {
let span = Span::raw("abc\u{200B}");
let mut buf = Buffer::empty(Rect::new(0, 0, 3, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[Cell::new("a"), Cell::new("b"), Cell::new("c\u{200B}")]
);
}
}
/// Regression test for <https://github.com/ratatui-org/ratatui/issues/1160> One line contains
/// some Unicode Left-Right-Marks (U+200E)
///
/// The issue was that a zero-width character at the end of the buffer causes the buffer bounds
/// to be exceeded (due to a position + 1 calculation that fails to account for the possibility
/// that the next position might not be available).
#[test]
fn issue_1160() {
let span = Span::raw("Hello\u{200E}");
let mut buf = Buffer::empty(Rect::new(0, 0, 5, 1));
span.render(buf.area, &mut buf);
assert_eq!(
buf.content(),
[
Cell::new("H"),
Cell::new("e"),
Cell::new("l"),
Cell::new("l"),
Cell::new("o\u{200E}"),
]
);
}
}

View File

@@ -3,7 +3,7 @@ use std::{borrow::Cow, fmt};
use itertools::{Itertools, Position};
use crate::{prelude::*, style::Styled};
use crate::prelude::*;
/// A string split over one or more lines.
///

View File

@@ -52,7 +52,7 @@ pub use self::{
table::{Cell, HighlightSpacing, Row, Table, TableState},
tabs::Tabs,
};
use crate::{buffer::Buffer, layout::Rect, style::Style};
use crate::{buffer::Buffer, layout::Rect};
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
///
@@ -245,6 +245,11 @@ pub trait StatefulWidget {
/// provided. This is a convenience approach to make it easier to attach child widgets to parent
/// widgets. It allows you to render an optional widget by reference.
///
/// A blanket [implementation of `WidgetRef` for `Fn(Rect, &mut
/// Buffer)`](WidgetRef#impl-WidgetRef-for-F) is provided. This allows you to treat any function or
/// closure that takes a `Rect` and a mutable reference to a `Buffer` as a widget in situations
/// where you don't want to create a new type for a widget.
///
/// # Examples
///
/// ```rust
@@ -304,13 +309,40 @@ pub trait WidgetRef {
fn render_ref(&self, area: Rect, buf: &mut Buffer);
}
/// This allows you to render a widget by reference.
// /// This allows you to render a widget by reference.
impl<W: WidgetRef> Widget for &W {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// A blanket implementation of `WidgetRef` for `Fn(Rect, &mut Buffer)`.
///
/// This allows you to treat any function that takes a `Rect` and a mutable reference to a `Buffer`
/// as a widget in situations where you don't want to create a new type for a widget.
///
/// # Examples
///
/// ```rust
/// use ratatui::{prelude::*, widgets::*};
/// fn hello(area: Rect, buf: &mut Buffer) {
/// Line::raw("Hello").render(area, buf);
/// }
///
/// fn draw(mut frame: Frame) {
/// frame.render_widget(&hello, frame.size());
/// frame.render_widget_ref(hello, frame.size());
/// }
/// ```
impl<F> WidgetRef for F
where
F: Fn(Rect, &mut Buffer),
{
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self(area, buf);
}
}
/// A blanket implementation of `WidgetExt` for `Option<W>` where `W` implements `WidgetRef`.
///
/// This is a convenience implementation that makes it easy to attach child widgets to parent
@@ -444,7 +476,7 @@ impl Widget for &str {
/// [`Rect`].
impl WidgetRef for &str {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
buf.set_string(area.x, area.y, self, crate::style::Style::default());
}
}
@@ -465,7 +497,7 @@ impl Widget for String {
/// without the need to give up ownership of the underlying text.
impl WidgetRef for String {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
buf.set_string(area.x, area.y, self, crate::style::Style::default());
}
}
@@ -474,7 +506,7 @@ mod tests {
use rstest::{fixture, rstest};
use super::*;
use crate::text::Line;
use crate::prelude::*;
#[fixture]
fn buf() -> Buffer {
@@ -650,13 +682,6 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
"hello world, just hello".render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
@@ -684,13 +709,6 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_area(mut buf: Buffer) {
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
String::from("hello world, just hello").render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);
@@ -709,4 +727,42 @@ mod tests {
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
mod function_widget {
use super::*;
fn widget_function(area: Rect, buf: &mut Buffer) {
"Hello".render(area, buf);
}
#[rstest]
fn render(mut buf: Buffer) {
widget_function.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
widget_function.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_closure(mut buf: Buffer) {
let widget = |area: Rect, buf: &mut Buffer| {
"Hello".render(area, buf);
};
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_closure_ref(mut buf: Buffer) {
let widget = |area: Rect, buf: &mut Buffer| {
"Hello".render(area, buf);
};
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
}
}

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, style::Styled, widgets::Block};
use crate::{prelude::*, widgets::Block};
mod bar;
mod bar_group;

View File

@@ -78,8 +78,7 @@ impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
impl<'a, const N: usize> From<&[(&'a str, u64); N]> for BarGroup<'a> {
fn from(value: &[(&'a str, u64); N]) -> Self {
let value: &[(&'a str, u64)] = value.as_ref();
Self::from(value)
Self::from(value.as_ref())
}
}

View File

@@ -8,7 +8,7 @@
use itertools::Itertools;
use strum::{Display, EnumString};
use crate::{prelude::*, style::Styled, symbols::border, widgets::Borders};
use crate::{prelude::*, symbols::border, widgets::Borders};
mod padding;
pub mod title;
@@ -53,10 +53,7 @@ pub use title::{Position, Title};
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// widgets::{block::*, *},
/// };
///
/// Block::new()
@@ -357,10 +354,7 @@ impl<'a> Block<'a> {
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// widgets::{block::*, *},
/// };
///
/// Block::new()

View File

@@ -36,10 +36,7 @@ use crate::{layout::Alignment, text::Line};
/// ```
/// use ratatui::{
/// prelude::*,
/// widgets::{
/// block::{Position, Title},
/// Block,
/// },
/// widgets::{block::*, *},
/// };
///
/// Title::from("Title")

View File

@@ -30,7 +30,7 @@ pub use self::{
points::Points,
rectangle::Rectangle,
};
use crate::{prelude::*, symbols::Marker, text::Line as TextLine, widgets::Block};
use crate::{prelude::*, text::Line as TextLine, widgets::Block};
/// Something that can be drawn on a [`Canvas`].
///
@@ -464,17 +464,17 @@ impl<'a> Context<'a> {
height: u16,
x_bounds: [f64; 2],
y_bounds: [f64; 2],
marker: Marker,
marker: symbols::Marker,
) -> Self {
let dot = symbols::DOT.chars().next().unwrap();
let block = symbols::block::FULL.chars().next().unwrap();
let bar = symbols::bar::HALF.chars().next().unwrap();
let grid: Box<dyn Grid> = match marker {
Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
Marker::Block => Box::new(CharGrid::new(width, height, block)),
Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
Marker::Braille => Box::new(BrailleGrid::new(width, height)),
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
symbols::Marker::Dot => Box::new(CharGrid::new(width, height, dot)),
symbols::Marker::Block => Box::new(CharGrid::new(width, height, block)),
symbols::Marker::Bar => Box::new(CharGrid::new(width, height, bar)),
symbols::Marker::Braille => Box::new(BrailleGrid::new(width, height)),
symbols::Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
};
Self {
x_bounds,
@@ -604,7 +604,7 @@ where
y_bounds: [f64; 2],
paint_func: Option<F>,
background_color: Color,
marker: Marker,
marker: symbols::Marker,
}
impl<'a, F> Default for Canvas<'a, F>
@@ -618,7 +618,7 @@ where
y_bounds: [0.0, 0.0],
paint_func: None,
background_color: Color::Reset,
marker: Marker::Braille,
marker: symbols::Marker::Braille,
}
}
}
@@ -717,7 +717,7 @@ where
/// .paint(|ctx| {});
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn marker(mut self, marker: Marker) -> Self {
pub const fn marker(mut self, marker: symbols::Marker) -> Self {
self.marker = marker;
self
}

View File

@@ -114,14 +114,8 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
mod tests {
use rstest::rstest;
use super::*;
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
symbols::Marker,
widgets::{canvas::Canvas, Widget},
};
use super::{super::*, *};
use crate::{buffer::Buffer, layout::Rect};
#[rstest]
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]

View File

@@ -65,7 +65,7 @@ mod tests {
use strum::ParseError;
use super::*;
use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas};
use crate::{prelude::*, widgets::canvas::Canvas};
#[test]
fn map_resolution_to_string() {

View File

@@ -66,7 +66,7 @@ impl Shape for Rectangle {
#[cfg(test)]
mod tests {
use super::*;
use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas};
use crate::{prelude::*, widgets::canvas::Canvas};
#[test]
fn draw_block_lines() {

View File

@@ -6,7 +6,6 @@ use unicode_width::UnicodeWidthStr;
use crate::{
layout::Flex,
prelude::*,
style::Styled,
widgets::{
canvas::{Canvas, Line as CanvasLine, Points},
Block,
@@ -286,7 +285,7 @@ impl LegendPosition {
/// This example draws a red line between two points.
///
/// ```rust
/// use ratatui::{prelude::*, symbols::Marker, widgets::*};
/// use ratatui::{prelude::*, widgets::*};
///
/// let dataset = Dataset::default()
/// .name("dataset 1")

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, style::Styled, widgets::Block};
use crate::{prelude::*, widgets::Block};
/// A widget to display a progress bar.
///

View File

@@ -3,7 +3,6 @@ use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
style::Styled,
widgets::{Block, HighlightSpacing},
};

View File

@@ -2,7 +2,6 @@ use unicode_width::UnicodeWidthStr;
use crate::{
prelude::*,
style::Styled,
text::StyledGrapheme,
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
@@ -433,7 +432,7 @@ mod test {
#[test]
fn zero_width_char_at_end_of_line() {
let line = "foo\u{200B}";
let line = "foo\0";
for paragraph in [
Paragraph::new(line),
Paragraph::new(line).wrap(Wrap { trim: false }),

View File

@@ -673,11 +673,11 @@ mod test {
#[test]
fn line_composer_zero_width_at_end() {
let width = 3;
let line = "foo\u{200B}";
let line = "foo\0";
let (word_wrapper, _, _) = run_composer(Composer::WordWrapper { trim: true }, line, width);
let (line_truncator, _, _) = run_composer(Composer::LineTruncator, line, width);
assert_eq!(word_wrapper, vec!["foo"]);
assert_eq!(line_truncator, vec!["foo\u{200B}"]);
assert_eq!(word_wrapper, vec!["foo\0"]);
assert_eq!(line_truncator, vec!["foo\0"]);
}
#[test]

View File

@@ -2,7 +2,7 @@ use std::cmp::min;
use strum::{Display, EnumString};
use crate::{prelude::*, style::Styled, widgets::Block};
use crate::{prelude::*, widgets::Block};
/// Widget to render a sparkline over one or more lines.
///

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, style::Styled};
use crate::prelude::*;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
///

View File

@@ -1,5 +1,5 @@
use super::Cell;
use crate::{prelude::*, style::Styled};
use crate::prelude::*;
/// A single row of data to be displayed in a [`Table`] widget.
///

View File

@@ -3,7 +3,7 @@ use itertools::Itertools;
#[allow(unused_imports)] // `Cell` is used in the doc comment but not the code
use super::Cell;
use super::{HighlightSpacing, Row, TableState};
use crate::{layout::Flex, prelude::*, style::Styled, widgets::Block};
use crate::{layout::Flex, prelude::*, widgets::Block};
/// A widget to display data in formatted columns.
///
@@ -20,9 +20,7 @@ use crate::{layout::Flex, prelude::*, style::Styled, widgets::Block};
/// [`Table`] implements [`Widget`] and so it can be drawn using [`Frame::render_widget`].
///
/// [`Table`] is also a [`StatefulWidget`], which means you can use it with [`TableState`] to allow
/// the user to scroll through the rows and select one of them. When rendering a [`Table`] with a
/// [`TableState`], the selected row will be highlighted. If the selected row is not visible (based
/// on the offset), the table will be scrolled to make the selected row visible.
/// the user to scroll through the rows and select one of them.
///
/// Note: if the `widths` field is empty, the table will be rendered with equal widths.
///
@@ -777,14 +775,7 @@ impl Table<'_> {
end += 1;
}
let Some(selected) = selected else {
return (start, end);
};
// clamp the selected row to the last row
let selected = selected.min(self.rows.len() - 1);
// scroll down until the selected row is visible
let selected = selected.unwrap_or(0).min(self.rows.len() - 1);
while selected >= end {
height = height.saturating_add(self.rows[end].height_with_margin());
end += 1;
@@ -793,8 +784,6 @@ impl Table<'_> {
start += 1;
}
}
// scroll up until the selected row is visible
while selected < start {
start -= 1;
height = height.saturating_add(self.rows[start].height_with_margin());
@@ -1018,8 +1007,6 @@ mod tests {
#[cfg(test)]
mod render {
use rstest::rstest;
use super::*;
#[test]
@@ -1210,36 +1197,6 @@ mod tests {
]);
assert_eq!(buf, expected);
}
/// Note that this includes a regression test for a bug where the table would not render the
/// correct rows when there is no selection.
/// <https://github.com/ratatui-org/ratatui/issues/1179>
#[rstest]
#[case::no_selection(None, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_before_offset(20, 20, ["20", "21", "22", "23", "24"])]
#[case::selection_immediately_before_offset(49, 49, ["49", "50", "51", "52", "53"])]
#[case::selection_at_start_of_offset(50, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_at_end_of_offset(54, 50, ["50", "51", "52", "53", "54"])]
#[case::selection_immediately_after_offset(55, 51, ["51", "52", "53", "54", "55"])]
#[case::selection_after_offset(80, 76, ["76", "77", "78", "79", "80"])]
fn render_with_selection_and_offset<T: Into<Option<usize>>>(
#[case] selected_row: T,
#[case] expected_offset: usize,
#[case] expected_items: [&str; 5],
) {
// render 100 rows offset at 50, with a selected row
let rows = (0..100).map(|i| Row::new([i.to_string()]));
let table = Table::new(rows, [Constraint::Length(2)]);
let mut buf = Buffer::empty(Rect::new(0, 0, 2, 5));
let mut state = TableState::new()
.with_offset(50)
.with_selected(selected_row);
StatefulWidget::render(table.clone(), Rect::new(0, 0, 5, 5), &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected_items));
assert_eq!(state.offset, expected_offset);
}
}
// test how constraints interact with table column width allocation

View File

@@ -1,4 +1,4 @@
use crate::{prelude::*, style::Styled, widgets::Block};
use crate::{prelude::*, widgets::Block};
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);