Compare commits
1 Commits
jm/crosste
...
jm/widget-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f99c224fc1 |
@@ -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)
|
||||
|
||||
26
Cargo.toml
26
Cargo.toml
@@ -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"]
|
||||
|
||||
18
README.md
18
README.md
@@ -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.
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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))?;
|
||||
|
||||
@@ -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"]));
|
||||
}
|
||||
|
||||
@@ -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(), " ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use self::layout::Position;
|
||||
use crate::prelude::*;
|
||||
|
||||
/// An iterator over rows within a `Rect`.
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
37
src/lib.rs
37
src/lib.rs
@@ -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;
|
||||
|
||||
@@ -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},
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
361
src/symbols.rs
361
src/symbols.rs
@@ -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 {
|
||||
|
||||
@@ -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!(
|
||||
"░░░░░░
|
||||
░ ░
|
||||
░ ░░ ░
|
||||
░ ░░ ░
|
||||
░ ░
|
||||
░░░░░░"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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!(
|
||||
"┏━┳┓
|
||||
┃ ┃┃
|
||||
┣━╋┫
|
||||
┗━┻┛"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 })?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
121
src/text/span.rs
121
src/text/span.rs
@@ -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}"),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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 "]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{prelude::*, style::Styled, widgets::Block};
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
|
||||
mod bar;
|
||||
mod bar_group;
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -36,10 +36,7 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// },
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
///
|
||||
/// Title::from("Title")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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])]
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{prelude::*, style::Styled, widgets::Block};
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
|
||||
/// A widget to display a progress bar.
|
||||
///
|
||||
|
||||
@@ -3,7 +3,6 @@ use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
style::Styled,
|
||||
widgets::{Block, HighlightSpacing},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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`].
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user