mirror of
https://github.com/atuinsh/atuin.git
synced 2025-01-14 10:19:06 +01:00
chore: remove tui vendoring (#804)
This commit is contained in:
parent
378be6b790
commit
ba1d615f5e
21
Cargo.lock
generated
21
Cargo.lock
generated
@ -83,8 +83,6 @@ dependencies = [
|
|||||||
"atuin-common",
|
"atuin-common",
|
||||||
"atuin-server",
|
"atuin-server",
|
||||||
"base64 0.20.0",
|
"base64 0.20.0",
|
||||||
"bitflags",
|
|
||||||
"cassowary",
|
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
@ -99,6 +97,7 @@ dependencies = [
|
|||||||
"interim",
|
"interim",
|
||||||
"itertools",
|
"itertools",
|
||||||
"log",
|
"log",
|
||||||
|
"ratatui",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"runtime-format",
|
"runtime-format",
|
||||||
"semver",
|
"semver",
|
||||||
@ -108,7 +107,6 @@ dependencies = [
|
|||||||
"tiny-bip39",
|
"tiny-bip39",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"unicode-segmentation",
|
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
"whoami",
|
"whoami",
|
||||||
]
|
]
|
||||||
@ -1763,6 +1761,19 @@ dependencies = [
|
|||||||
"getrandom",
|
"getrandom",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui"
|
||||||
|
version = "0.20.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dcc0d032bccba900ee32151ec0265667535c230169f5a011154cdcd984e16829"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"cassowary",
|
||||||
|
"crossterm",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rayon"
|
name = "rayon"
|
||||||
version = "1.6.1"
|
version = "1.6.1"
|
||||||
@ -2790,9 +2801,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.9.0"
|
version = "1.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
|
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-width"
|
name = "unicode-width"
|
||||||
|
@ -74,11 +74,7 @@ runtime-format = "0.1.2"
|
|||||||
tiny-bip39 = "1"
|
tiny-bip39 = "1"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
skim = { version = "0.10.2", default-features = false }
|
skim = { version = "0.10.2", default-features = false }
|
||||||
|
ratatui = "0.20.1"
|
||||||
# from tui
|
|
||||||
bitflags = "1.3"
|
|
||||||
cassowary = "0.3"
|
|
||||||
unicode-segmentation = "1.2"
|
|
||||||
|
|
||||||
[dependencies.tracing-subscriber]
|
[dependencies.tracing-subscriber]
|
||||||
version = "0.3"
|
version = "0.3"
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use crate::tui::{
|
use atuin_client::history::History;
|
||||||
|
use ratatui::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
widgets::{Block, StatefulWidget, Widget},
|
widgets::{Block, StatefulWidget, Widget},
|
||||||
};
|
};
|
||||||
use atuin_client::history::History;
|
|
||||||
|
|
||||||
use super::{format_duration, interactive::HistoryWrapper};
|
use super::{format_duration, interactive::HistoryWrapper};
|
||||||
|
|
||||||
|
@ -26,16 +26,14 @@ use super::{
|
|||||||
cursor::Cursor,
|
cursor::Cursor,
|
||||||
history_list::{HistoryList, ListState, PREFIX_LENGTH},
|
history_list::{HistoryList, ListState, PREFIX_LENGTH},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::VERSION;
|
||||||
tui::{
|
use ratatui::{
|
||||||
backend::{Backend, CrosstermBackend},
|
backend::{Backend, CrosstermBackend},
|
||||||
layout::{Alignment, Constraint, Direction, Layout},
|
layout::{Alignment, Constraint, Direction, Layout},
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Span, Spans, Text},
|
text::{Span, Spans, Text},
|
||||||
widgets::{Block, BorderType, Borders, Paragraph},
|
widgets::{Block, BorderType, Borders, Paragraph},
|
||||||
Frame, Terminal,
|
Frame, Terminal,
|
||||||
},
|
|
||||||
VERSION,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const RETURN_ORIGINAL: usize = usize::MAX;
|
const RETURN_ORIGINAL: usize = usize::MAX;
|
||||||
|
@ -6,7 +6,6 @@ use eyre::Result;
|
|||||||
|
|
||||||
use command::AtuinCmd;
|
use command::AtuinCmd;
|
||||||
mod command;
|
mod command;
|
||||||
mod tui;
|
|
||||||
|
|
||||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2016 Florian Dehau
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
@ -1,5 +0,0 @@
|
|||||||
# tui-rs
|
|
||||||
|
|
||||||
A fork of https://crates.io/crates/tui/0.19.0 since it is now unmaintained.
|
|
||||||
|
|
||||||
Some parts have been removed or modified for simplicity, but it is currently mostly equivalent.
|
|
@ -1,221 +0,0 @@
|
|||||||
use crate::tui::{
|
|
||||||
backend::Backend,
|
|
||||||
buffer::Cell,
|
|
||||||
layout::Rect,
|
|
||||||
style::{Color, Modifier},
|
|
||||||
};
|
|
||||||
use crossterm::{
|
|
||||||
cursor::{Hide, MoveTo, Show},
|
|
||||||
execute, queue,
|
|
||||||
style::{
|
|
||||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
|
||||||
SetForegroundColor,
|
|
||||||
},
|
|
||||||
terminal::{self, Clear, ClearType},
|
|
||||||
};
|
|
||||||
use std::io::{self, Write};
|
|
||||||
|
|
||||||
pub struct CrosstermBackend<W: Write> {
|
|
||||||
buffer: W,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W> CrosstermBackend<W>
|
|
||||||
where
|
|
||||||
W: Write,
|
|
||||||
{
|
|
||||||
pub fn new(buffer: W) -> CrosstermBackend<W> {
|
|
||||||
CrosstermBackend { buffer }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W> Write for CrosstermBackend<W>
|
|
||||||
where
|
|
||||||
W: Write,
|
|
||||||
{
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
self.buffer.write(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.buffer.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<W> Backend for CrosstermBackend<W>
|
|
||||||
where
|
|
||||||
W: Write,
|
|
||||||
{
|
|
||||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
|
||||||
where
|
|
||||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
|
||||||
{
|
|
||||||
let mut fg = Color::Reset;
|
|
||||||
let mut bg = Color::Reset;
|
|
||||||
let mut modifier = Modifier::empty();
|
|
||||||
let mut last_pos: Option<(u16, u16)> = None;
|
|
||||||
for (x, y, cell) in content {
|
|
||||||
// Move the cursor if the previous location was not (x - 1, y)
|
|
||||||
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
|
||||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
|
||||||
}
|
|
||||||
last_pos = Some((x, y));
|
|
||||||
if cell.modifier != modifier {
|
|
||||||
let diff = ModifierDiff {
|
|
||||||
from: modifier,
|
|
||||||
to: cell.modifier,
|
|
||||||
};
|
|
||||||
diff.queue(&mut self.buffer)?;
|
|
||||||
modifier = cell.modifier;
|
|
||||||
}
|
|
||||||
if cell.fg != fg {
|
|
||||||
let color = CColor::from(cell.fg);
|
|
||||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
|
||||||
fg = cell.fg;
|
|
||||||
}
|
|
||||||
if cell.bg != bg {
|
|
||||||
let color = CColor::from(cell.bg);
|
|
||||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
|
||||||
bg = cell.bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
map_error(queue!(
|
|
||||||
self.buffer,
|
|
||||||
SetForegroundColor(CColor::Reset),
|
|
||||||
SetBackgroundColor(CColor::Reset),
|
|
||||||
SetAttribute(CAttribute::Reset)
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
|
||||||
map_error(execute!(self.buffer, Hide))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_cursor(&mut self) -> io::Result<()> {
|
|
||||||
map_error(execute!(self.buffer, Show))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
|
||||||
crossterm::cursor::position()
|
|
||||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
|
||||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear(&mut self) -> io::Result<()> {
|
|
||||||
map_error(execute!(self.buffer, Clear(ClearType::All)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size(&self) -> io::Result<Rect> {
|
|
||||||
let (width, height) =
|
|
||||||
terminal::size().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(Rect::new(0, 0, width, height))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.buffer.flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
|
|
||||||
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Color> for CColor {
|
|
||||||
fn from(color: Color) -> Self {
|
|
||||||
match color {
|
|
||||||
Color::Reset => CColor::Reset,
|
|
||||||
Color::Black => CColor::Black,
|
|
||||||
Color::Red => CColor::DarkRed,
|
|
||||||
Color::Green => CColor::DarkGreen,
|
|
||||||
Color::Yellow => CColor::DarkYellow,
|
|
||||||
Color::Blue => CColor::DarkBlue,
|
|
||||||
Color::Magenta => CColor::DarkMagenta,
|
|
||||||
Color::Cyan => CColor::DarkCyan,
|
|
||||||
Color::Gray => CColor::Grey,
|
|
||||||
Color::DarkGray => CColor::DarkGrey,
|
|
||||||
Color::LightRed => CColor::Red,
|
|
||||||
Color::LightGreen => CColor::Green,
|
|
||||||
Color::LightBlue => CColor::Blue,
|
|
||||||
Color::LightYellow => CColor::Yellow,
|
|
||||||
Color::LightMagenta => CColor::Magenta,
|
|
||||||
Color::LightCyan => CColor::Cyan,
|
|
||||||
Color::White => CColor::White,
|
|
||||||
Color::Indexed(i) => CColor::AnsiValue(i),
|
|
||||||
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct ModifierDiff {
|
|
||||||
pub from: Modifier,
|
|
||||||
pub to: Modifier,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ModifierDiff {
|
|
||||||
fn queue<W>(&self, mut w: W) -> io::Result<()>
|
|
||||||
where
|
|
||||||
W: io::Write,
|
|
||||||
{
|
|
||||||
//use crossterm::Attribute;
|
|
||||||
let removed = self.from - self.to;
|
|
||||||
if removed.contains(Modifier::REVERSED) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
|
||||||
}
|
|
||||||
if removed.contains(Modifier::BOLD) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
|
||||||
if self.to.contains(Modifier::DIM) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if removed.contains(Modifier::ITALIC) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
|
||||||
}
|
|
||||||
if removed.contains(Modifier::UNDERLINED) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
|
||||||
}
|
|
||||||
if removed.contains(Modifier::DIM) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
|
||||||
}
|
|
||||||
if removed.contains(Modifier::CROSSED_OUT) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
|
||||||
}
|
|
||||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let added = self.to - self.from;
|
|
||||||
if added.contains(Modifier::REVERSED) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
|
||||||
}
|
|
||||||
if added.contains(Modifier::BOLD) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
|
||||||
}
|
|
||||||
if added.contains(Modifier::ITALIC) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
|
||||||
}
|
|
||||||
if added.contains(Modifier::UNDERLINED) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
|
||||||
}
|
|
||||||
if added.contains(Modifier::DIM) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
|
||||||
}
|
|
||||||
if added.contains(Modifier::CROSSED_OUT) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
|
||||||
}
|
|
||||||
if added.contains(Modifier::SLOW_BLINK) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
|
||||||
}
|
|
||||||
if added.contains(Modifier::RAPID_BLINK) {
|
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
|
|
||||||
use crate::tui::buffer::Cell;
|
|
||||||
use crate::tui::layout::Rect;
|
|
||||||
|
|
||||||
mod crossterm;
|
|
||||||
pub use self::crossterm::CrosstermBackend;
|
|
||||||
|
|
||||||
pub trait Backend {
|
|
||||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
|
||||||
where
|
|
||||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
|
||||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
|
||||||
fn show_cursor(&mut self) -> Result<(), io::Error>;
|
|
||||||
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
|
||||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
|
||||||
fn clear(&mut self) -> Result<(), io::Error>;
|
|
||||||
fn size(&self) -> Result<Rect, io::Error>;
|
|
||||||
fn flush(&mut self) -> Result<(), io::Error>;
|
|
||||||
}
|
|
@ -1,734 +0,0 @@
|
|||||||
use crate::tui::{
|
|
||||||
layout::Rect,
|
|
||||||
style::{Color, Modifier, Style},
|
|
||||||
text::{Span, Spans},
|
|
||||||
};
|
|
||||||
use std::cmp::min;
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
/// A buffer cell
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Cell {
|
|
||||||
pub symbol: String,
|
|
||||||
pub fg: Color,
|
|
||||||
pub bg: Color,
|
|
||||||
pub modifier: Modifier,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cell {
|
|
||||||
pub fn set_symbol(&mut self, symbol: &str) -> &mut Cell {
|
|
||||||
self.symbol.clear();
|
|
||||||
self.symbol.push_str(symbol);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_char(&mut self, ch: char) -> &mut Cell {
|
|
||||||
self.symbol.clear();
|
|
||||||
self.symbol.push(ch);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_fg(&mut self, color: Color) -> &mut Cell {
|
|
||||||
self.fg = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_bg(&mut self, color: Color) -> &mut Cell {
|
|
||||||
self.bg = color;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_style(&mut self, style: Style) -> &mut Cell {
|
|
||||||
if let Some(c) = style.fg {
|
|
||||||
self.fg = c;
|
|
||||||
}
|
|
||||||
if let Some(c) = style.bg {
|
|
||||||
self.bg = c;
|
|
||||||
}
|
|
||||||
self.modifier.insert(style.add_modifier);
|
|
||||||
self.modifier.remove(style.sub_modifier);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn style(&self) -> Style {
|
|
||||||
Style::default()
|
|
||||||
.fg(self.fg)
|
|
||||||
.bg(self.bg)
|
|
||||||
.add_modifier(self.modifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
self.symbol.clear();
|
|
||||||
self.symbol.push(' ');
|
|
||||||
self.fg = Color::Reset;
|
|
||||||
self.bg = Color::Reset;
|
|
||||||
self.modifier = Modifier::empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Cell {
|
|
||||||
fn default() -> Cell {
|
|
||||||
Cell {
|
|
||||||
symbol: " ".into(),
|
|
||||||
fg: Color::Reset,
|
|
||||||
bg: Color::Reset,
|
|
||||||
modifier: Modifier::empty(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
|
||||||
///
|
|
||||||
/// No widget in the library interacts directly with the terminal. Instead each of them is required
|
|
||||||
/// to draw their state to an intermediate buffer. It is basically a grid where each cell contains
|
|
||||||
/// a grapheme, a foreground color and a background color. This grid will then be used to output
|
|
||||||
/// the appropriate escape sequences and characters to draw the UI as the user has defined it.
|
|
||||||
///
|
|
||||||
/// # Examples:
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use tui::buffer::{Buffer, Cell};
|
|
||||||
/// use tui::layout::Rect;
|
|
||||||
/// use tui::style::{Color, Style, Modifier};
|
|
||||||
///
|
|
||||||
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
|
|
||||||
/// buf.get_mut(0, 2).set_symbol("x");
|
|
||||||
/// assert_eq!(buf.get(0, 2).symbol, "x");
|
|
||||||
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
|
|
||||||
/// assert_eq!(buf.get(5, 0), &Cell{
|
|
||||||
/// symbol: String::from("r"),
|
|
||||||
/// fg: Color::Red,
|
|
||||||
/// bg: Color::White,
|
|
||||||
/// modifier: Modifier::empty()
|
|
||||||
/// });
|
|
||||||
/// buf.get_mut(5, 0).set_char('x');
|
|
||||||
/// assert_eq!(buf.get(5, 0).symbol, "x");
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
||||||
pub struct Buffer {
|
|
||||||
/// The area represented by this buffer
|
|
||||||
pub area: Rect,
|
|
||||||
/// The content of the buffer. The length of this Vec should always be equal to area.width *
|
|
||||||
/// area.height
|
|
||||||
pub content: Vec<Cell>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Buffer {
|
|
||||||
/// Returns a Buffer with all cells set to the default one
|
|
||||||
pub fn empty(area: Rect) -> Buffer {
|
|
||||||
let cell = Cell::default();
|
|
||||||
Buffer::filled(area, &cell)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a Buffer with all cells initialized with the attributes of the given Cell
|
|
||||||
pub fn filled(area: Rect, cell: &Cell) -> Buffer {
|
|
||||||
let size = area.area() as usize;
|
|
||||||
let mut content = Vec::with_capacity(size);
|
|
||||||
for _ in 0..size {
|
|
||||||
content.push(cell.clone());
|
|
||||||
}
|
|
||||||
Buffer { area, content }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a Buffer containing the given lines
|
|
||||||
pub fn with_lines<S>(lines: &[S]) -> Buffer
|
|
||||||
where
|
|
||||||
S: AsRef<str>,
|
|
||||||
{
|
|
||||||
let height = lines.len() as u16;
|
|
||||||
let width = lines
|
|
||||||
.iter()
|
|
||||||
.map(|i| i.as_ref().width() as u16)
|
|
||||||
.max()
|
|
||||||
.unwrap_or_default();
|
|
||||||
let mut buffer = Buffer::empty(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
});
|
|
||||||
for (y, line) in lines.iter().enumerate() {
|
|
||||||
buffer.set_string(0, y as u16, line, Style::default());
|
|
||||||
}
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the content of the buffer as a slice
|
|
||||||
pub fn content(&self) -> &[Cell] {
|
|
||||||
&self.content
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the area covered by this buffer
|
|
||||||
pub fn area(&self) -> &Rect {
|
|
||||||
&self.area
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a reference to Cell at the given coordinates
|
|
||||||
pub fn get(&self, x: u16, y: u16) -> &Cell {
|
|
||||||
let i = self.index_of(x, y);
|
|
||||||
&self.content[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a mutable reference to Cell at the given coordinates
|
|
||||||
pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
|
||||||
let i = self.index_of(x, y);
|
|
||||||
&mut self.content[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
|
|
||||||
///
|
|
||||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use tui::buffer::Buffer;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// let rect = Rect::new(200, 100, 10, 10);
|
|
||||||
/// let buffer = Buffer::empty(rect);
|
|
||||||
/// // Global coordinates to the top corner of this buffer's area
|
|
||||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
|
||||||
///
|
|
||||||
/// ```should_panic
|
|
||||||
/// # use tui::buffer::Buffer;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// let rect = Rect::new(200, 100, 10, 10);
|
|
||||||
/// let buffer = Buffer::empty(rect);
|
|
||||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
|
||||||
/// // starts at (200, 100).
|
|
||||||
/// buffer.index_of(0, 0); // Panics
|
|
||||||
/// ```
|
|
||||||
pub fn index_of(&self, x: u16, y: u16) -> usize {
|
|
||||||
debug_assert!(
|
|
||||||
x >= self.area.left()
|
|
||||||
&& x < self.area.right()
|
|
||||||
&& y >= self.area.top()
|
|
||||||
&& y < self.area.bottom(),
|
|
||||||
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
self.area
|
|
||||||
);
|
|
||||||
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the (global) coordinates of a cell given its index
|
|
||||||
///
|
|
||||||
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use tui::buffer::Buffer;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// let rect = Rect::new(200, 100, 10, 10);
|
|
||||||
/// let buffer = Buffer::empty(rect);
|
|
||||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
|
||||||
/// assert_eq!(buffer.pos_of(14), (204, 101));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// Panics when given an index that is outside the Buffer's content.
|
|
||||||
///
|
|
||||||
/// ```should_panic
|
|
||||||
/// # use tui::buffer::Buffer;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
|
||||||
/// let buffer = Buffer::empty(rect);
|
|
||||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
|
||||||
/// buffer.pos_of(100); // Panics
|
|
||||||
/// ```
|
|
||||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
|
||||||
debug_assert!(
|
|
||||||
i < self.content.len(),
|
|
||||||
"Trying to get the coords of a cell outside the buffer: i={} len={}",
|
|
||||||
i,
|
|
||||||
self.content.len()
|
|
||||||
);
|
|
||||||
(
|
|
||||||
self.area.x + i as u16 % self.area.width,
|
|
||||||
self.area.y + i as u16 / self.area.width,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print a string, starting at the position (x, y)
|
|
||||||
pub fn set_string<S>(&mut self, x: u16, y: u16, string: S, style: Style)
|
|
||||||
where
|
|
||||||
S: AsRef<str>,
|
|
||||||
{
|
|
||||||
self.set_stringn(x, y, string, usize::MAX, style);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Print at most the first n characters of a string if enough space is available
|
|
||||||
/// until the end of the line
|
|
||||||
pub fn set_stringn<S>(
|
|
||||||
&mut self,
|
|
||||||
x: u16,
|
|
||||||
y: u16,
|
|
||||||
string: S,
|
|
||||||
width: usize,
|
|
||||||
style: Style,
|
|
||||||
) -> (u16, u16)
|
|
||||||
where
|
|
||||||
S: AsRef<str>,
|
|
||||||
{
|
|
||||||
let mut index = self.index_of(x, y);
|
|
||||||
let mut x_offset = x as usize;
|
|
||||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
|
||||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
|
||||||
for s in graphemes {
|
|
||||||
let width = s.width();
|
|
||||||
if width == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we
|
|
||||||
// change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32.
|
|
||||||
if width > max_offset.saturating_sub(x_offset) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.content[index].set_symbol(s);
|
|
||||||
self.content[index].set_style(style);
|
|
||||||
// Reset following cells if multi-width (they would be hidden by the grapheme),
|
|
||||||
for i in index + 1..index + width {
|
|
||||||
self.content[i].reset();
|
|
||||||
}
|
|
||||||
index += width;
|
|
||||||
x_offset += width;
|
|
||||||
}
|
|
||||||
(x_offset as u16, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_spans(&mut self, x: u16, y: u16, spans: &Spans<'_>, width: u16) -> (u16, u16) {
|
|
||||||
let mut remaining_width = width;
|
|
||||||
let mut x = x;
|
|
||||||
for span in &spans.0 {
|
|
||||||
if remaining_width == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let pos = self.set_stringn(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
span.content.as_ref(),
|
|
||||||
remaining_width as usize,
|
|
||||||
span.style,
|
|
||||||
);
|
|
||||||
let w = pos.0.saturating_sub(x);
|
|
||||||
x = pos.0;
|
|
||||||
remaining_width = remaining_width.saturating_sub(w);
|
|
||||||
}
|
|
||||||
(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_span(&mut self, x: u16, y: u16, span: &Span<'_>, width: u16) -> (u16, u16) {
|
|
||||||
self.set_stringn(x, y, span.content.as_ref(), width as usize, span.style)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[deprecated(
|
|
||||||
since = "0.10.0",
|
|
||||||
note = "You should use styling capabilities of `Buffer::set_style`"
|
|
||||||
)]
|
|
||||||
pub fn set_background(&mut self, area: Rect, color: Color) {
|
|
||||||
for y in area.top()..area.bottom() {
|
|
||||||
for x in area.left()..area.right() {
|
|
||||||
self.get_mut(x, y).set_bg(color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_style(&mut self, area: Rect, style: Style) {
|
|
||||||
for y in area.top()..area.bottom() {
|
|
||||||
for x in area.left()..area.right() {
|
|
||||||
self.get_mut(x, y).set_style(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resize the buffer so that the mapped area matches the given area and that the buffer
|
|
||||||
/// length is equal to area.width * area.height
|
|
||||||
pub fn resize(&mut self, area: Rect) {
|
|
||||||
let length = area.area() as usize;
|
|
||||||
if self.content.len() > length {
|
|
||||||
self.content.truncate(length);
|
|
||||||
} else {
|
|
||||||
self.content.resize(length, Cell::default());
|
|
||||||
}
|
|
||||||
self.area = area;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Reset all cells in the buffer
|
|
||||||
pub fn reset(&mut self) {
|
|
||||||
for c in &mut self.content {
|
|
||||||
c.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Merge an other buffer into this one
|
|
||||||
pub fn merge(&mut self, other: &Buffer) {
|
|
||||||
let area = self.area.union(other.area);
|
|
||||||
let cell = Cell::default();
|
|
||||||
self.content.resize(area.area() as usize, cell.clone());
|
|
||||||
|
|
||||||
// Move original content to the appropriate space
|
|
||||||
let size = self.area.area() as usize;
|
|
||||||
for i in (0..size).rev() {
|
|
||||||
let (x, y) = self.pos_of(i);
|
|
||||||
// New index in content
|
|
||||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
|
||||||
if i != k {
|
|
||||||
self.content[k] = self.content[i].clone();
|
|
||||||
self.content[i] = cell.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push content of the other buffer into this one (may erase previous
|
|
||||||
// data)
|
|
||||||
let size = other.area.area() as usize;
|
|
||||||
for i in 0..size {
|
|
||||||
let (x, y) = other.pos_of(i);
|
|
||||||
// New index in content
|
|
||||||
let k = ((y - area.y) * area.width + x - area.x) as usize;
|
|
||||||
self.content[k] = other.content[i].clone();
|
|
||||||
}
|
|
||||||
self.area = area;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a minimal sequence of coordinates and Cells necessary to update the UI from
|
|
||||||
/// self to other.
|
|
||||||
///
|
|
||||||
/// We're assuming that buffers are well-formed, that is no double-width cell is followed by
|
|
||||||
/// a non-blank cell.
|
|
||||||
///
|
|
||||||
/// # Multi-width characters handling:
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// (Index:) `01`
|
|
||||||
/// Prev: `コ`
|
|
||||||
/// Next: `aa`
|
|
||||||
/// Updates: `0: a, 1: a'
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// (Index:) `01`
|
|
||||||
/// Prev: `a `
|
|
||||||
/// Next: `コ`
|
|
||||||
/// Updates: `0: コ` (double width symbol at index 0 - skip index 1)
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// (Index:) `012`
|
|
||||||
/// Prev: `aaa`
|
|
||||||
/// Next: `aコ`
|
|
||||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
|
||||||
/// ```
|
|
||||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
|
||||||
let previous_buffer = &self.content;
|
|
||||||
let next_buffer = &other.content;
|
|
||||||
let width = self.area.width;
|
|
||||||
|
|
||||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
|
||||||
// Cells invalidated by drawing/replacing preceeding multi-width characters:
|
|
||||||
let mut invalidated: usize = 0;
|
|
||||||
// Cells from the current buffer to skip due to preceeding multi-width characters taking their
|
|
||||||
// place (the skipped cells should be blank anyway):
|
|
||||||
let mut to_skip: usize = 0;
|
|
||||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
|
||||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
|
||||||
let x = i as u16 % width;
|
|
||||||
let y = i as u16 / width;
|
|
||||||
updates.push((x, y, &next_buffer[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
to_skip = current.symbol.width().saturating_sub(1);
|
|
||||||
|
|
||||||
let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
|
|
||||||
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
|
|
||||||
}
|
|
||||||
updates
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn cell(s: &str) -> Cell {
|
|
||||||
let mut cell = Cell::default();
|
|
||||||
cell.set_symbol(s);
|
|
||||||
cell
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_translates_to_and_from_coordinates() {
|
|
||||||
let rect = Rect::new(200, 100, 50, 80);
|
|
||||||
let buf = Buffer::empty(rect);
|
|
||||||
|
|
||||||
// First cell is at the upper left corner.
|
|
||||||
assert_eq!(buf.pos_of(0), (200, 100));
|
|
||||||
assert_eq!(buf.index_of(200, 100), 0);
|
|
||||||
|
|
||||||
// Last cell is in the lower right.
|
|
||||||
assert_eq!(buf.pos_of(buf.content.len() - 1), (249, 179));
|
|
||||||
assert_eq!(buf.index_of(249, 179), buf.content.len() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
#[should_panic(expected = "outside the buffer")]
|
|
||||||
fn pos_of_panics_on_out_of_bounds() {
|
|
||||||
let rect = Rect::new(0, 0, 10, 10);
|
|
||||||
let buf = Buffer::empty(rect);
|
|
||||||
|
|
||||||
// There are a total of 100 cells; zero-indexed means that 100 would be the 101st cell.
|
|
||||||
buf.pos_of(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
#[should_panic(expected = "outside the buffer")]
|
|
||||||
fn index_of_panics_on_out_of_bounds() {
|
|
||||||
let rect = Rect::new(0, 0, 10, 10);
|
|
||||||
let buf = Buffer::empty(rect);
|
|
||||||
|
|
||||||
// width is 10; zero-indexed means that 10 would be the 11th cell.
|
|
||||||
buf.index_of(10, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_set_string() {
|
|
||||||
let area = Rect::new(0, 0, 5, 1);
|
|
||||||
let mut buffer = Buffer::empty(area);
|
|
||||||
|
|
||||||
// Zero-width
|
|
||||||
buffer.set_stringn(0, 0, "aaa", 0, Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&[" "]));
|
|
||||||
|
|
||||||
buffer.set_string(0, 0, "aaa", Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&["aaa "]));
|
|
||||||
|
|
||||||
// Width limit:
|
|
||||||
buffer.set_stringn(0, 0, "bbbbbbbbbbbbbb", 4, Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&["bbbb "]));
|
|
||||||
|
|
||||||
buffer.set_string(0, 0, "12345", Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&["12345"]));
|
|
||||||
|
|
||||||
// Width truncation:
|
|
||||||
buffer.set_string(0, 0, "123456", Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&["12345"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_set_string_zero_width() {
|
|
||||||
let area = Rect::new(0, 0, 1, 1);
|
|
||||||
let mut buffer = Buffer::empty(area);
|
|
||||||
|
|
||||||
// Leading grapheme with zero width
|
|
||||||
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{1}";
|
|
||||||
buffer.set_stringn(0, 0, s, 1, Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&["a"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_set_string_double_width() {
|
|
||||||
let area = Rect::new(0, 0, 5, 1);
|
|
||||||
let mut buffer = Buffer::empty(area);
|
|
||||||
buffer.set_string(0, 0, "コン", Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&["コン "]));
|
|
||||||
|
|
||||||
// Only 1 space left.
|
|
||||||
buffer.set_string(0, 0, "コンピ", Style::default());
|
|
||||||
assert_eq!(buffer, Buffer::with_lines(&["コン "]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_with_lines() {
|
|
||||||
let buffer = Buffer::with_lines(&["┌────────┐", "│コンピュ│", "│ーa 上で│", "└────────┘"]);
|
|
||||||
assert_eq!(buffer.area.x, 0);
|
|
||||||
assert_eq!(buffer.area.y, 0);
|
|
||||||
assert_eq!(buffer.area.width, 10);
|
|
||||||
assert_eq!(buffer.area.height, 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_diffing_empty_empty() {
|
|
||||||
let area = Rect::new(0, 0, 40, 40);
|
|
||||||
let prev = Buffer::empty(area);
|
|
||||||
let next = Buffer::empty(area);
|
|
||||||
let diff = prev.diff(&next);
|
|
||||||
assert_eq!(diff, vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_diffing_empty_filled() {
|
|
||||||
let area = Rect::new(0, 0, 40, 40);
|
|
||||||
let prev = Buffer::empty(area);
|
|
||||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
|
||||||
let diff = prev.diff(&next);
|
|
||||||
assert_eq!(diff.len(), 40 * 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_diffing_filled_filled() {
|
|
||||||
let area = Rect::new(0, 0, 40, 40);
|
|
||||||
let prev = Buffer::filled(area, Cell::default().set_symbol("a"));
|
|
||||||
let next = Buffer::filled(area, Cell::default().set_symbol("a"));
|
|
||||||
let diff = prev.diff(&next);
|
|
||||||
assert_eq!(diff, vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_diffing_single_width() {
|
|
||||||
let prev = Buffer::with_lines(&[
|
|
||||||
" ",
|
|
||||||
"┌Title─┐ ",
|
|
||||||
"│ │ ",
|
|
||||||
"│ │ ",
|
|
||||||
"└──────┘ ",
|
|
||||||
]);
|
|
||||||
let next = Buffer::with_lines(&[
|
|
||||||
" ",
|
|
||||||
"┌TITLE─┐ ",
|
|
||||||
"│ │ ",
|
|
||||||
"│ │ ",
|
|
||||||
"└──────┘ ",
|
|
||||||
]);
|
|
||||||
let diff = prev.diff(&next);
|
|
||||||
assert_eq!(
|
|
||||||
diff,
|
|
||||||
vec![
|
|
||||||
(2, 1, &cell("I")),
|
|
||||||
(3, 1, &cell("T")),
|
|
||||||
(4, 1, &cell("L")),
|
|
||||||
(5, 1, &cell("E")),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[rustfmt::skip]
|
|
||||||
fn buffer_diffing_multi_width() {
|
|
||||||
let prev = Buffer::with_lines(&[
|
|
||||||
"┌Title─┐ ",
|
|
||||||
"└──────┘ ",
|
|
||||||
]);
|
|
||||||
let next = Buffer::with_lines(&[
|
|
||||||
"┌称号──┐ ",
|
|
||||||
"└──────┘ ",
|
|
||||||
]);
|
|
||||||
let diff = prev.diff(&next);
|
|
||||||
assert_eq!(
|
|
||||||
diff,
|
|
||||||
vec![
|
|
||||||
(1, 0, &cell("称")),
|
|
||||||
// Skipped "i"
|
|
||||||
(3, 0, &cell("号")),
|
|
||||||
// Skipped "l"
|
|
||||||
(5, 0, &cell("─")),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_diffing_multi_width_offset() {
|
|
||||||
let prev = Buffer::with_lines(&["┌称号──┐"]);
|
|
||||||
let next = Buffer::with_lines(&["┌─称号─┐"]);
|
|
||||||
|
|
||||||
let diff = prev.diff(&next);
|
|
||||||
assert_eq!(
|
|
||||||
diff,
|
|
||||||
vec![(1, 0, &cell("─")), (2, 0, &cell("称")), (4, 0, &cell("号")),]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_merge() {
|
|
||||||
let mut one = Buffer::filled(
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 2,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
Cell::default().set_symbol("1"),
|
|
||||||
);
|
|
||||||
let two = Buffer::filled(
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 2,
|
|
||||||
width: 2,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
Cell::default().set_symbol("2"),
|
|
||||||
);
|
|
||||||
one.merge(&two);
|
|
||||||
assert_eq!(one, Buffer::with_lines(&["11", "11", "22", "22"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_merge2() {
|
|
||||||
let mut one = Buffer::filled(
|
|
||||||
Rect {
|
|
||||||
x: 2,
|
|
||||||
y: 2,
|
|
||||||
width: 2,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
Cell::default().set_symbol("1"),
|
|
||||||
);
|
|
||||||
let two = Buffer::filled(
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 2,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
Cell::default().set_symbol("2"),
|
|
||||||
);
|
|
||||||
one.merge(&two);
|
|
||||||
assert_eq!(one, Buffer::with_lines(&["22 ", "22 ", " 11", " 11"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn buffer_merge3() {
|
|
||||||
let mut one = Buffer::filled(
|
|
||||||
Rect {
|
|
||||||
x: 3,
|
|
||||||
y: 3,
|
|
||||||
width: 2,
|
|
||||||
height: 2,
|
|
||||||
},
|
|
||||||
Cell::default().set_symbol("1"),
|
|
||||||
);
|
|
||||||
let two = Buffer::filled(
|
|
||||||
Rect {
|
|
||||||
x: 1,
|
|
||||||
y: 1,
|
|
||||||
width: 3,
|
|
||||||
height: 4,
|
|
||||||
},
|
|
||||||
Cell::default().set_symbol("2"),
|
|
||||||
);
|
|
||||||
one.merge(&two);
|
|
||||||
let mut merged = Buffer::with_lines(&["222 ", "222 ", "2221", "2221"]);
|
|
||||||
merged.area = Rect {
|
|
||||||
x: 1,
|
|
||||||
y: 1,
|
|
||||||
width: 4,
|
|
||||||
height: 4,
|
|
||||||
};
|
|
||||||
assert_eq!(one, merged);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,537 +0,0 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::cmp::{max, min};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use cassowary::strength::{REQUIRED, WEAK};
|
|
||||||
use cassowary::WeightedRelation::{EQ, GE, LE};
|
|
||||||
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Corner {
|
|
||||||
TopLeft,
|
|
||||||
TopRight,
|
|
||||||
BottomRight,
|
|
||||||
BottomLeft,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Hash, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Direction {
|
|
||||||
Horizontal,
|
|
||||||
Vertical,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum Constraint {
|
|
||||||
// TODO: enforce range 0 - 100
|
|
||||||
Percentage(u16),
|
|
||||||
Ratio(u32, u32),
|
|
||||||
Length(u16),
|
|
||||||
Max(u16),
|
|
||||||
Min(u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Constraint {
|
|
||||||
pub fn apply(&self, length: u16) -> u16 {
|
|
||||||
match *self {
|
|
||||||
Constraint::Percentage(p) => length * p / 100,
|
|
||||||
Constraint::Ratio(num, den) => {
|
|
||||||
let r = num * u32::from(length) / den;
|
|
||||||
r as u16
|
|
||||||
}
|
|
||||||
Constraint::Length(l) => length.min(l),
|
|
||||||
Constraint::Max(m) => length.min(m),
|
|
||||||
Constraint::Min(m) => length.max(m),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Margin {
|
|
||||||
pub vertical: u16,
|
|
||||||
pub horizontal: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Alignment {
|
|
||||||
Left,
|
|
||||||
Center,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct Layout {
|
|
||||||
direction: Direction,
|
|
||||||
margin: Margin,
|
|
||||||
constraints: Vec<Constraint>,
|
|
||||||
/// Whether the last chunk of the computed layout should be expanded to fill the available
|
|
||||||
/// space.
|
|
||||||
expand_to_fill: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout), Vec<Rect>>> = RefCell::new(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Layout {
|
|
||||||
fn default() -> Layout {
|
|
||||||
Layout {
|
|
||||||
direction: Direction::Vertical,
|
|
||||||
margin: Margin {
|
|
||||||
horizontal: 0,
|
|
||||||
vertical: 0,
|
|
||||||
},
|
|
||||||
constraints: Vec::new(),
|
|
||||||
expand_to_fill: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Layout {
|
|
||||||
pub fn constraints<C>(mut self, constraints: C) -> Layout
|
|
||||||
where
|
|
||||||
C: Into<Vec<Constraint>>,
|
|
||||||
{
|
|
||||||
self.constraints = constraints.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn margin(mut self, margin: u16) -> Layout {
|
|
||||||
self.margin = Margin {
|
|
||||||
horizontal: margin,
|
|
||||||
vertical: margin,
|
|
||||||
};
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
|
|
||||||
self.margin.horizontal = horizontal;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn vertical_margin(mut self, vertical: u16) -> Layout {
|
|
||||||
self.margin.vertical = vertical;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn direction(mut self, direction: Direction) -> Layout {
|
|
||||||
self.direction = direction;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn expand_to_fill(mut self, expand_to_fill: bool) -> Layout {
|
|
||||||
self.expand_to_fill = expand_to_fill;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrapper function around the cassowary-rs solver to be able to split a given
|
|
||||||
/// area into smaller ones based on the preferred widths or heights and the direction.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use tui::layout::{Rect, Constraint, Direction, Layout};
|
|
||||||
/// let chunks = Layout::default()
|
|
||||||
/// .direction(Direction::Vertical)
|
|
||||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
|
||||||
/// .split(Rect {
|
|
||||||
/// x: 2,
|
|
||||||
/// y: 2,
|
|
||||||
/// width: 10,
|
|
||||||
/// height: 10,
|
|
||||||
/// });
|
|
||||||
/// assert_eq!(
|
|
||||||
/// chunks,
|
|
||||||
/// vec![
|
|
||||||
/// Rect {
|
|
||||||
/// x: 2,
|
|
||||||
/// y: 2,
|
|
||||||
/// width: 10,
|
|
||||||
/// height: 5
|
|
||||||
/// },
|
|
||||||
/// Rect {
|
|
||||||
/// x: 2,
|
|
||||||
/// y: 7,
|
|
||||||
/// width: 10,
|
|
||||||
/// height: 5
|
|
||||||
/// }
|
|
||||||
/// ]
|
|
||||||
/// );
|
|
||||||
///
|
|
||||||
/// let chunks = Layout::default()
|
|
||||||
/// .direction(Direction::Horizontal)
|
|
||||||
/// .constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)].as_ref())
|
|
||||||
/// .split(Rect {
|
|
||||||
/// x: 0,
|
|
||||||
/// y: 0,
|
|
||||||
/// width: 9,
|
|
||||||
/// height: 2,
|
|
||||||
/// });
|
|
||||||
/// assert_eq!(
|
|
||||||
/// chunks,
|
|
||||||
/// vec![
|
|
||||||
/// Rect {
|
|
||||||
/// x: 0,
|
|
||||||
/// y: 0,
|
|
||||||
/// width: 3,
|
|
||||||
/// height: 2
|
|
||||||
/// },
|
|
||||||
/// Rect {
|
|
||||||
/// x: 3,
|
|
||||||
/// y: 0,
|
|
||||||
/// width: 6,
|
|
||||||
/// height: 2
|
|
||||||
/// }
|
|
||||||
/// ]
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn split(&self, area: Rect) -> Vec<Rect> {
|
|
||||||
// TODO: Maybe use a fixed size cache ?
|
|
||||||
LAYOUT_CACHE.with(|c| {
|
|
||||||
c.borrow_mut()
|
|
||||||
.entry((area, self.clone()))
|
|
||||||
.or_insert_with(|| split(area, self))
|
|
||||||
.clone()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn split(area: Rect, layout: &Layout) -> Vec<Rect> {
|
|
||||||
let mut solver = Solver::new();
|
|
||||||
let mut vars: HashMap<Variable, (usize, usize)> = HashMap::new();
|
|
||||||
let elements = layout
|
|
||||||
.constraints
|
|
||||||
.iter()
|
|
||||||
.map(|_| Element::new())
|
|
||||||
.collect::<Vec<Element>>();
|
|
||||||
let mut results = layout
|
|
||||||
.constraints
|
|
||||||
.iter()
|
|
||||||
.map(|_| Rect::default())
|
|
||||||
.collect::<Vec<Rect>>();
|
|
||||||
|
|
||||||
let dest_area = area.inner(&layout.margin);
|
|
||||||
for (i, e) in elements.iter().enumerate() {
|
|
||||||
vars.insert(e.x, (i, 0));
|
|
||||||
vars.insert(e.y, (i, 1));
|
|
||||||
vars.insert(e.width, (i, 2));
|
|
||||||
vars.insert(e.height, (i, 3));
|
|
||||||
}
|
|
||||||
let mut ccs: Vec<CassowaryConstraint> =
|
|
||||||
Vec::with_capacity(elements.len() * 4 + layout.constraints.len() * 6);
|
|
||||||
for elt in &elements {
|
|
||||||
ccs.push(elt.width | GE(REQUIRED) | 0f64);
|
|
||||||
ccs.push(elt.height | GE(REQUIRED) | 0f64);
|
|
||||||
ccs.push(elt.left() | GE(REQUIRED) | f64::from(dest_area.left()));
|
|
||||||
ccs.push(elt.top() | GE(REQUIRED) | f64::from(dest_area.top()));
|
|
||||||
ccs.push(elt.right() | LE(REQUIRED) | f64::from(dest_area.right()));
|
|
||||||
ccs.push(elt.bottom() | LE(REQUIRED) | f64::from(dest_area.bottom()));
|
|
||||||
}
|
|
||||||
if let Some(first) = elements.first() {
|
|
||||||
ccs.push(match layout.direction {
|
|
||||||
Direction::Horizontal => first.left() | EQ(REQUIRED) | f64::from(dest_area.left()),
|
|
||||||
Direction::Vertical => first.top() | EQ(REQUIRED) | f64::from(dest_area.top()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if layout.expand_to_fill {
|
|
||||||
if let Some(last) = elements.last() {
|
|
||||||
ccs.push(match layout.direction {
|
|
||||||
Direction::Horizontal => last.right() | EQ(REQUIRED) | f64::from(dest_area.right()),
|
|
||||||
Direction::Vertical => last.bottom() | EQ(REQUIRED) | f64::from(dest_area.bottom()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match layout.direction {
|
|
||||||
Direction::Horizontal => {
|
|
||||||
for pair in elements.windows(2) {
|
|
||||||
ccs.push((pair[0].x + pair[0].width) | EQ(REQUIRED) | pair[1].x);
|
|
||||||
}
|
|
||||||
for (i, size) in layout.constraints.iter().enumerate() {
|
|
||||||
ccs.push(elements[i].y | EQ(REQUIRED) | f64::from(dest_area.y));
|
|
||||||
ccs.push(elements[i].height | EQ(REQUIRED) | f64::from(dest_area.height));
|
|
||||||
ccs.push(match *size {
|
|
||||||
Constraint::Length(v) => elements[i].width | EQ(WEAK) | f64::from(v),
|
|
||||||
Constraint::Percentage(v) => {
|
|
||||||
elements[i].width | EQ(WEAK) | (f64::from(v * dest_area.width) / 100.0)
|
|
||||||
}
|
|
||||||
Constraint::Ratio(n, d) => {
|
|
||||||
elements[i].width
|
|
||||||
| EQ(WEAK)
|
|
||||||
| (f64::from(dest_area.width) * f64::from(n) / f64::from(d))
|
|
||||||
}
|
|
||||||
Constraint::Min(v) => elements[i].width | GE(WEAK) | f64::from(v),
|
|
||||||
Constraint::Max(v) => elements[i].width | LE(WEAK) | f64::from(v),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Direction::Vertical => {
|
|
||||||
for pair in elements.windows(2) {
|
|
||||||
ccs.push((pair[0].y + pair[0].height) | EQ(REQUIRED) | pair[1].y);
|
|
||||||
}
|
|
||||||
for (i, size) in layout.constraints.iter().enumerate() {
|
|
||||||
ccs.push(elements[i].x | EQ(REQUIRED) | f64::from(dest_area.x));
|
|
||||||
ccs.push(elements[i].width | EQ(REQUIRED) | f64::from(dest_area.width));
|
|
||||||
ccs.push(match *size {
|
|
||||||
Constraint::Length(v) => elements[i].height | EQ(WEAK) | f64::from(v),
|
|
||||||
Constraint::Percentage(v) => {
|
|
||||||
elements[i].height | EQ(WEAK) | (f64::from(v * dest_area.height) / 100.0)
|
|
||||||
}
|
|
||||||
Constraint::Ratio(n, d) => {
|
|
||||||
elements[i].height
|
|
||||||
| EQ(WEAK)
|
|
||||||
| (f64::from(dest_area.height) * f64::from(n) / f64::from(d))
|
|
||||||
}
|
|
||||||
Constraint::Min(v) => elements[i].height | GE(WEAK) | f64::from(v),
|
|
||||||
Constraint::Max(v) => elements[i].height | LE(WEAK) | f64::from(v),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
solver.add_constraints(&ccs).unwrap();
|
|
||||||
for &(var, value) in solver.fetch_changes() {
|
|
||||||
let (index, attr) = vars[&var];
|
|
||||||
let value = if value.is_sign_negative() {
|
|
||||||
0
|
|
||||||
} else {
|
|
||||||
value as u16
|
|
||||||
};
|
|
||||||
match attr {
|
|
||||||
0 => {
|
|
||||||
results[index].x = value;
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
results[index].y = value;
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
results[index].width = value;
|
|
||||||
}
|
|
||||||
3 => {
|
|
||||||
results[index].height = value;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if layout.expand_to_fill {
|
|
||||||
// Fix imprecision by extending the last item a bit if necessary
|
|
||||||
if let Some(last) = results.last_mut() {
|
|
||||||
match layout.direction {
|
|
||||||
Direction::Vertical => {
|
|
||||||
last.height = dest_area.bottom() - last.y;
|
|
||||||
}
|
|
||||||
Direction::Horizontal => {
|
|
||||||
last.width = dest_area.right() - last.x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A container used by the solver inside split
|
|
||||||
struct Element {
|
|
||||||
x: Variable,
|
|
||||||
y: Variable,
|
|
||||||
width: Variable,
|
|
||||||
height: Variable,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Element {
|
|
||||||
fn new() -> Element {
|
|
||||||
Element {
|
|
||||||
x: Variable::new(),
|
|
||||||
y: Variable::new(),
|
|
||||||
width: Variable::new(),
|
|
||||||
height: Variable::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn left(&self) -> Variable {
|
|
||||||
self.x
|
|
||||||
}
|
|
||||||
|
|
||||||
fn top(&self) -> Variable {
|
|
||||||
self.y
|
|
||||||
}
|
|
||||||
|
|
||||||
fn right(&self) -> Expression {
|
|
||||||
self.x + self.width
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bottom(&self) -> Expression {
|
|
||||||
self.y + self.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A simple rectangle used in the computation of the layout and to give widgets a hint about the
|
|
||||||
/// area they are supposed to render to.
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Default)]
|
|
||||||
pub struct Rect {
|
|
||||||
pub x: u16,
|
|
||||||
pub y: u16,
|
|
||||||
pub width: u16,
|
|
||||||
pub height: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
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) -> Rect {
|
|
||||||
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);
|
|
||||||
let max_area_f = f64::from(max_area);
|
|
||||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
|
||||||
let width_f = height_f * aspect_ratio;
|
|
||||||
(width_f as u16, height_f as u16)
|
|
||||||
} else {
|
|
||||||
(width, height)
|
|
||||||
};
|
|
||||||
Rect {
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width: clipped_width,
|
|
||||||
height: clipped_height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn area(self) -> u16 {
|
|
||||||
self.width * self.height
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn left(self) -> u16 {
|
|
||||||
self.x
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn right(self) -> u16 {
|
|
||||||
self.x.saturating_add(self.width)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn top(self) -> u16 {
|
|
||||||
self.y
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bottom(self) -> u16 {
|
|
||||||
self.y.saturating_add(self.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inner(self, margin: &Margin) -> Rect {
|
|
||||||
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
|
|
||||||
Rect::default()
|
|
||||||
} else {
|
|
||||||
Rect {
|
|
||||||
x: self.x + margin.horizontal,
|
|
||||||
y: self.y + margin.vertical,
|
|
||||||
width: self.width - 2 * margin.horizontal,
|
|
||||||
height: self.height - 2 * margin.vertical,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn union(self, other: Rect) -> Rect {
|
|
||||||
let x1 = min(self.x, other.x);
|
|
||||||
let y1 = min(self.y, other.y);
|
|
||||||
let x2 = max(self.x + self.width, other.x + other.width);
|
|
||||||
let y2 = max(self.y + self.height, other.y + other.height);
|
|
||||||
Rect {
|
|
||||||
x: x1,
|
|
||||||
y: y1,
|
|
||||||
width: x2 - x1,
|
|
||||||
height: y2 - y1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn intersection(self, other: Rect) -> Rect {
|
|
||||||
let x1 = max(self.x, other.x);
|
|
||||||
let y1 = max(self.y, other.y);
|
|
||||||
let x2 = min(self.x + self.width, other.x + other.width);
|
|
||||||
let y2 = min(self.y + self.height, other.y + other.height);
|
|
||||||
Rect {
|
|
||||||
x: x1,
|
|
||||||
y: y1,
|
|
||||||
width: x2 - x1,
|
|
||||||
height: y2 - y1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn intersects(self, other: Rect) -> bool {
|
|
||||||
self.x < other.x + other.width
|
|
||||||
&& self.x + self.width > other.x
|
|
||||||
&& self.y < other.y + other.height
|
|
||||||
&& self.y + self.height > other.y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vertical_split_by_height() {
|
|
||||||
let target = Rect {
|
|
||||||
x: 2,
|
|
||||||
y: 2,
|
|
||||||
width: 10,
|
|
||||||
height: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
let chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.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>());
|
|
||||||
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rect_size_truncation() {
|
|
||||||
for width in 256u16..300u16 {
|
|
||||||
for height in 256u16..300u16 {
|
|
||||||
let rect = Rect::new(0, 0, width, height);
|
|
||||||
rect.area(); // Should not panic.
|
|
||||||
assert!(rect.width < width || rect.height < height);
|
|
||||||
// The target dimensions are rounded down so the math will not be too precise
|
|
||||||
// but let's make sure the ratios don't diverge crazily.
|
|
||||||
assert!(
|
|
||||||
(f64::from(rect.width) / f64::from(rect.height)
|
|
||||||
- f64::from(width) / f64::from(height))
|
|
||||||
.abs()
|
|
||||||
< 1.0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One dimension below 255, one above. Area above max u16.
|
|
||||||
let width = 900;
|
|
||||||
let height = 100;
|
|
||||||
let rect = Rect::new(0, 0, width, height);
|
|
||||||
assert_ne!(rect.width, 900);
|
|
||||||
assert_ne!(rect.height, 100);
|
|
||||||
assert!(rect.width < width || rect.height < height);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_rect_size_preservation() {
|
|
||||||
for width in 0..256u16 {
|
|
||||||
for height in 0..256u16 {
|
|
||||||
let rect = Rect::new(0, 0, width, height);
|
|
||||||
rect.area(); // Should not panic.
|
|
||||||
assert_eq!(rect.width, width);
|
|
||||||
assert_eq!(rect.height, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One dimension below 255, one above. Area below max u16.
|
|
||||||
let rect = Rect::new(0, 0, 300, 100);
|
|
||||||
assert_eq!(rect.width, 300);
|
|
||||||
assert_eq!(rect.height, 100);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
//! Fork of `tui-rs`
|
|
||||||
#![allow(
|
|
||||||
clippy::module_name_repetitions,
|
|
||||||
clippy::bool_to_int_with_if,
|
|
||||||
clippy::similar_names,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_sign_loss,
|
|
||||||
dead_code
|
|
||||||
)]
|
|
||||||
|
|
||||||
pub mod backend;
|
|
||||||
pub mod buffer;
|
|
||||||
pub mod layout;
|
|
||||||
pub mod style;
|
|
||||||
pub mod symbols;
|
|
||||||
pub mod terminal;
|
|
||||||
pub mod text;
|
|
||||||
pub mod widgets;
|
|
||||||
|
|
||||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
|
|
278
src/tui/style.rs
278
src/tui/style.rs
@ -1,278 +0,0 @@
|
|||||||
//! `style` contains the primitives used to control how your user interface will look.
|
|
||||||
|
|
||||||
use bitflags::bitflags;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Color {
|
|
||||||
Reset,
|
|
||||||
Black,
|
|
||||||
Red,
|
|
||||||
Green,
|
|
||||||
Yellow,
|
|
||||||
Blue,
|
|
||||||
Magenta,
|
|
||||||
Cyan,
|
|
||||||
Gray,
|
|
||||||
DarkGray,
|
|
||||||
LightRed,
|
|
||||||
LightGreen,
|
|
||||||
LightYellow,
|
|
||||||
LightBlue,
|
|
||||||
LightMagenta,
|
|
||||||
LightCyan,
|
|
||||||
White,
|
|
||||||
Rgb(u8, u8, u8),
|
|
||||||
Indexed(u8),
|
|
||||||
}
|
|
||||||
|
|
||||||
bitflags! {
|
|
||||||
/// Modifier changes the way a piece of text is displayed.
|
|
||||||
///
|
|
||||||
/// They are bitflags so they can easily be composed.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::style::Modifier;
|
|
||||||
///
|
|
||||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
|
||||||
/// ```
|
|
||||||
pub struct Modifier: u16 {
|
|
||||||
const BOLD = 0b0000_0000_0001;
|
|
||||||
const DIM = 0b0000_0000_0010;
|
|
||||||
const ITALIC = 0b0000_0000_0100;
|
|
||||||
const UNDERLINED = 0b0000_0000_1000;
|
|
||||||
const SLOW_BLINK = 0b0000_0001_0000;
|
|
||||||
const RAPID_BLINK = 0b0000_0010_0000;
|
|
||||||
const REVERSED = 0b0000_0100_0000;
|
|
||||||
const HIDDEN = 0b0000_1000_0000;
|
|
||||||
const CROSSED_OUT = 0b0001_0000_0000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Style let you control the main characteristics of the displayed elements.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// Style::default()
|
|
||||||
/// .fg(Color::Black)
|
|
||||||
/// .bg(Color::Green)
|
|
||||||
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
|
|
||||||
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
|
|
||||||
/// just S3.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// # use tui::buffer::Buffer;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// let styles = [
|
|
||||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
|
||||||
/// Style::default().bg(Color::Red),
|
|
||||||
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
|
|
||||||
/// ];
|
|
||||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
|
||||||
/// for style in &styles {
|
|
||||||
/// buffer.get_mut(0, 0).set_style(*style);
|
|
||||||
/// }
|
|
||||||
/// assert_eq!(
|
|
||||||
/// Style {
|
|
||||||
/// fg: Some(Color::Yellow),
|
|
||||||
/// bg: Some(Color::Red),
|
|
||||||
/// add_modifier: Modifier::BOLD,
|
|
||||||
/// sub_modifier: Modifier::empty(),
|
|
||||||
/// },
|
|
||||||
/// buffer.get(0, 0).style(),
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// The default implementation returns a `Style` that does not modify anything. If you wish to
|
|
||||||
/// reset all properties until that point use [`Style::reset`].
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// # use tui::buffer::Buffer;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// let styles = [
|
|
||||||
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
|
|
||||||
/// Style::reset().fg(Color::Yellow),
|
|
||||||
/// ];
|
|
||||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
|
|
||||||
/// for style in &styles {
|
|
||||||
/// buffer.get_mut(0, 0).set_style(*style);
|
|
||||||
/// }
|
|
||||||
/// assert_eq!(
|
|
||||||
/// Style {
|
|
||||||
/// fg: Some(Color::Yellow),
|
|
||||||
/// bg: Some(Color::Reset),
|
|
||||||
/// add_modifier: Modifier::empty(),
|
|
||||||
/// sub_modifier: Modifier::empty(),
|
|
||||||
/// },
|
|
||||||
/// buffer.get(0, 0).style(),
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct Style {
|
|
||||||
pub fg: Option<Color>,
|
|
||||||
pub bg: Option<Color>,
|
|
||||||
pub add_modifier: Modifier,
|
|
||||||
pub sub_modifier: Modifier,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Style {
|
|
||||||
fn default() -> Style {
|
|
||||||
Style {
|
|
||||||
fg: None,
|
|
||||||
bg: None,
|
|
||||||
add_modifier: Modifier::empty(),
|
|
||||||
sub_modifier: Modifier::empty(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Style {
|
|
||||||
/// Returns a `Style` resetting all properties.
|
|
||||||
pub fn reset() -> Style {
|
|
||||||
Style {
|
|
||||||
fg: Some(Color::Reset),
|
|
||||||
bg: Some(Color::Reset),
|
|
||||||
add_modifier: Modifier::empty(),
|
|
||||||
sub_modifier: Modifier::all(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes the foreground color.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::style::{Color, Style};
|
|
||||||
/// let style = Style::default().fg(Color::Blue);
|
|
||||||
/// let diff = Style::default().fg(Color::Red);
|
|
||||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
|
||||||
/// ```
|
|
||||||
pub fn fg(mut self, color: Color) -> Style {
|
|
||||||
self.fg = Some(color);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes the background color.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::style::{Color, Style};
|
|
||||||
/// let style = Style::default().bg(Color::Blue);
|
|
||||||
/// let diff = Style::default().bg(Color::Red);
|
|
||||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
|
||||||
/// ```
|
|
||||||
pub fn bg(mut self, color: Color) -> Style {
|
|
||||||
self.bg = Some(color);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes the text emphasis.
|
|
||||||
///
|
|
||||||
/// When applied, it adds the given modifier to the `Style` modifiers.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
|
||||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
|
||||||
/// let patched = style.patch(diff);
|
|
||||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
|
|
||||||
/// assert_eq!(patched.sub_modifier, Modifier::empty());
|
|
||||||
/// ```
|
|
||||||
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
|
|
||||||
self.sub_modifier.remove(modifier);
|
|
||||||
self.add_modifier.insert(modifier);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Changes the text emphasis.
|
|
||||||
///
|
|
||||||
/// When applied, it removes the given modifier from the `Style` modifiers.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
|
||||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
|
||||||
/// let patched = style.patch(diff);
|
|
||||||
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
|
|
||||||
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
|
|
||||||
/// ```
|
|
||||||
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
|
|
||||||
self.add_modifier.remove(modifier);
|
|
||||||
self.sub_modifier.insert(modifier);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Results in a combined style that is equivalent to applying the two individual styles to
|
|
||||||
/// a style one after the other.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
/// ```
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
|
||||||
/// let style_2 = Style::default().bg(Color::Red);
|
|
||||||
/// let combined = style_1.patch(style_2);
|
|
||||||
/// assert_eq!(
|
|
||||||
/// Style::default().patch(style_1).patch(style_2),
|
|
||||||
/// Style::default().patch(combined));
|
|
||||||
/// ```
|
|
||||||
pub fn patch(mut self, other: Style) -> Style {
|
|
||||||
self.fg = other.fg.or(self.fg);
|
|
||||||
self.bg = other.bg.or(self.bg);
|
|
||||||
|
|
||||||
self.add_modifier.remove(other.sub_modifier);
|
|
||||||
self.add_modifier.insert(other.add_modifier);
|
|
||||||
self.sub_modifier.remove(other.add_modifier);
|
|
||||||
self.sub_modifier.insert(other.sub_modifier);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn styles() -> Vec<Style> {
|
|
||||||
vec![
|
|
||||||
Style::default(),
|
|
||||||
Style::default().fg(Color::Yellow),
|
|
||||||
Style::default().bg(Color::Yellow),
|
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
|
||||||
Style::default().remove_modifier(Modifier::BOLD),
|
|
||||||
Style::default().add_modifier(Modifier::ITALIC),
|
|
||||||
Style::default().remove_modifier(Modifier::ITALIC),
|
|
||||||
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
|
|
||||||
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
|
||||||
let styles = styles();
|
|
||||||
for &a in &styles {
|
|
||||||
for &b in &styles {
|
|
||||||
for &c in &styles {
|
|
||||||
for &d in &styles {
|
|
||||||
let combined = a.patch(b.patch(c.patch(d)));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Style::default().patch(a).patch(b).patch(c).patch(d),
|
|
||||||
Style::default().patch(combined)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,233 +0,0 @@
|
|||||||
pub mod block {
|
|
||||||
pub const FULL: &str = "█";
|
|
||||||
pub const SEVEN_EIGHTHS: &str = "▉";
|
|
||||||
pub const THREE_QUARTERS: &str = "▊";
|
|
||||||
pub const FIVE_EIGHTHS: &str = "▋";
|
|
||||||
pub const HALF: &str = "▌";
|
|
||||||
pub const THREE_EIGHTHS: &str = "▍";
|
|
||||||
pub const ONE_QUARTER: &str = "▎";
|
|
||||||
pub const ONE_EIGHTH: &str = "▏";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Set {
|
|
||||||
pub full: &'static str,
|
|
||||||
pub seven_eighths: &'static str,
|
|
||||||
pub three_quarters: &'static str,
|
|
||||||
pub five_eighths: &'static str,
|
|
||||||
pub half: &'static str,
|
|
||||||
pub three_eighths: &'static str,
|
|
||||||
pub one_quarter: &'static str,
|
|
||||||
pub one_eighth: &'static str,
|
|
||||||
pub empty: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const THREE_LEVELS: Set = Set {
|
|
||||||
full: FULL,
|
|
||||||
seven_eighths: FULL,
|
|
||||||
three_quarters: HALF,
|
|
||||||
five_eighths: HALF,
|
|
||||||
half: HALF,
|
|
||||||
three_eighths: HALF,
|
|
||||||
one_quarter: HALF,
|
|
||||||
one_eighth: " ",
|
|
||||||
empty: " ",
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NINE_LEVELS: Set = Set {
|
|
||||||
full: FULL,
|
|
||||||
seven_eighths: SEVEN_EIGHTHS,
|
|
||||||
three_quarters: THREE_QUARTERS,
|
|
||||||
five_eighths: FIVE_EIGHTHS,
|
|
||||||
half: HALF,
|
|
||||||
three_eighths: THREE_EIGHTHS,
|
|
||||||
one_quarter: ONE_QUARTER,
|
|
||||||
one_eighth: ONE_EIGHTH,
|
|
||||||
empty: " ",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod bar {
|
|
||||||
pub const FULL: &str = "█";
|
|
||||||
pub const SEVEN_EIGHTHS: &str = "▇";
|
|
||||||
pub const THREE_QUARTERS: &str = "▆";
|
|
||||||
pub const FIVE_EIGHTHS: &str = "▅";
|
|
||||||
pub const HALF: &str = "▄";
|
|
||||||
pub const THREE_EIGHTHS: &str = "▃";
|
|
||||||
pub const ONE_QUARTER: &str = "▂";
|
|
||||||
pub const ONE_EIGHTH: &str = "▁";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Set {
|
|
||||||
pub full: &'static str,
|
|
||||||
pub seven_eighths: &'static str,
|
|
||||||
pub three_quarters: &'static str,
|
|
||||||
pub five_eighths: &'static str,
|
|
||||||
pub half: &'static str,
|
|
||||||
pub three_eighths: &'static str,
|
|
||||||
pub one_quarter: &'static str,
|
|
||||||
pub one_eighth: &'static str,
|
|
||||||
pub empty: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const THREE_LEVELS: Set = Set {
|
|
||||||
full: FULL,
|
|
||||||
seven_eighths: FULL,
|
|
||||||
three_quarters: HALF,
|
|
||||||
five_eighths: HALF,
|
|
||||||
half: HALF,
|
|
||||||
three_eighths: HALF,
|
|
||||||
one_quarter: HALF,
|
|
||||||
one_eighth: " ",
|
|
||||||
empty: " ",
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const NINE_LEVELS: Set = Set {
|
|
||||||
full: FULL,
|
|
||||||
seven_eighths: SEVEN_EIGHTHS,
|
|
||||||
three_quarters: THREE_QUARTERS,
|
|
||||||
five_eighths: FIVE_EIGHTHS,
|
|
||||||
half: HALF,
|
|
||||||
three_eighths: THREE_EIGHTHS,
|
|
||||||
one_quarter: ONE_QUARTER,
|
|
||||||
one_eighth: ONE_EIGHTH,
|
|
||||||
empty: " ",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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)]
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 const DOT: &str = "•";
|
|
||||||
|
|
||||||
pub mod braille {
|
|
||||||
pub const BLANK: u16 = 0x2800;
|
|
||||||
pub const DOTS: [[u16; 2]; 4] = [
|
|
||||||
[0x0001, 0x0008],
|
|
||||||
[0x0002, 0x0010],
|
|
||||||
[0x0004, 0x0020],
|
|
||||||
[0x0040, 0x0080],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Marker to use when plotting data points
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum Marker {
|
|
||||||
/// One point per cell in shape of dot
|
|
||||||
Dot,
|
|
||||||
/// One point per cell in shape of a block
|
|
||||||
Block,
|
|
||||||
/// Up to 8 points per cell
|
|
||||||
Braille,
|
|
||||||
}
|
|
@ -1,321 +0,0 @@
|
|||||||
use crate::tui::{
|
|
||||||
backend::Backend,
|
|
||||||
buffer::Buffer,
|
|
||||||
layout::Rect,
|
|
||||||
widgets::{StatefulWidget, Widget},
|
|
||||||
};
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
/// UNSTABLE
|
|
||||||
enum ResizeBehavior {
|
|
||||||
Fixed,
|
|
||||||
Auto,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
/// UNSTABLE
|
|
||||||
pub struct Viewport {
|
|
||||||
area: Rect,
|
|
||||||
resize_behavior: ResizeBehavior,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Viewport {
|
|
||||||
/// UNSTABLE
|
|
||||||
pub fn fixed(area: Rect) -> Viewport {
|
|
||||||
Viewport {
|
|
||||||
area,
|
|
||||||
resize_behavior: ResizeBehavior::Fixed,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
/// Options to pass to [`Terminal::with_options`]
|
|
||||||
pub struct TerminalOptions {
|
|
||||||
/// Viewport used to draw to the terminal
|
|
||||||
pub viewport: Viewport,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Interface to the terminal backed by Termion
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Terminal<B>
|
|
||||||
where
|
|
||||||
B: Backend,
|
|
||||||
{
|
|
||||||
backend: B,
|
|
||||||
/// Holds the results of the current and previous draw calls. The two are compared at the end
|
|
||||||
/// of each draw pass to output the necessary updates to the terminal
|
|
||||||
buffers: [Buffer; 2],
|
|
||||||
/// Index of the current buffer in the previous array
|
|
||||||
current: usize,
|
|
||||||
/// Whether the cursor is currently hidden
|
|
||||||
hidden_cursor: bool,
|
|
||||||
/// Viewport
|
|
||||||
viewport: Viewport,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a consistent terminal interface for rendering.
|
|
||||||
pub struct Frame<'a, B: 'a + Backend> {
|
|
||||||
terminal: &'a mut Terminal<B>,
|
|
||||||
|
|
||||||
/// Where should the cursor be after drawing this frame?
|
|
||||||
///
|
|
||||||
/// If `None`, the cursor is hidden and its position is controlled by the backend. If `Some((x,
|
|
||||||
/// y))`, the cursor is shown and placed at `(x, y)` after the call to `Terminal::draw()`.
|
|
||||||
cursor_position: Option<(u16, u16)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, B> Frame<'a, B>
|
|
||||||
where
|
|
||||||
B: Backend,
|
|
||||||
{
|
|
||||||
/// Terminal size, guaranteed not to change when rendering.
|
|
||||||
pub fn size(&self) -> Rect {
|
|
||||||
self.terminal.viewport.area
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a [`Widget`] to the current buffer using [`Widget::render`].
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::Terminal;
|
|
||||||
/// # use tui::backend::TestBackend;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// # use tui::widgets::Block;
|
|
||||||
/// # let backend = TestBackend::new(5, 5);
|
|
||||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
|
||||||
/// let block = Block::default();
|
|
||||||
/// let area = Rect::new(0, 0, 5, 5);
|
|
||||||
/// let mut frame = terminal.get_frame();
|
|
||||||
/// frame.render_widget(block, area);
|
|
||||||
/// ```
|
|
||||||
pub fn render_widget<W>(&mut self, widget: W, area: Rect)
|
|
||||||
where
|
|
||||||
W: Widget,
|
|
||||||
{
|
|
||||||
widget.render(area, self.terminal.current_buffer_mut());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Render a [`StatefulWidget`] to the current buffer using [`StatefulWidget::render`].
|
|
||||||
///
|
|
||||||
/// The last argument should be an instance of the [`StatefulWidget::State`] associated to the
|
|
||||||
/// given [`StatefulWidget`].
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::Terminal;
|
|
||||||
/// # use tui::backend::TestBackend;
|
|
||||||
/// # use tui::layout::Rect;
|
|
||||||
/// # use tui::widgets::{List, ListItem, ListState};
|
|
||||||
/// # let backend = TestBackend::new(5, 5);
|
|
||||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
|
||||||
/// let mut state = ListState::default();
|
|
||||||
/// state.select(Some(1));
|
|
||||||
/// let items = vec![
|
|
||||||
/// ListItem::new("Item 1"),
|
|
||||||
/// ListItem::new("Item 2"),
|
|
||||||
/// ];
|
|
||||||
/// let list = List::new(items);
|
|
||||||
/// let area = Rect::new(0, 0, 5, 5);
|
|
||||||
/// let mut frame = terminal.get_frame();
|
|
||||||
/// frame.render_stateful_widget(list, area, &mut state);
|
|
||||||
/// ```
|
|
||||||
pub fn render_stateful_widget<W>(&mut self, widget: W, area: Rect, state: &mut W::State)
|
|
||||||
where
|
|
||||||
W: StatefulWidget,
|
|
||||||
{
|
|
||||||
widget.render(area, self.terminal.current_buffer_mut(), state);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// After drawing this frame, make the cursor visible and put it at the specified (x, y)
|
|
||||||
/// coordinates. If this method is not called, the cursor will be hidden.
|
|
||||||
///
|
|
||||||
/// Note that this will interfere with calls to `Terminal::hide_cursor()`,
|
|
||||||
/// `Terminal::show_cursor()`, and `Terminal::set_cursor()`. Pick one of the APIs and stick
|
|
||||||
/// with it.
|
|
||||||
pub fn set_cursor(&mut self, x: u16, y: u16) {
|
|
||||||
self.cursor_position = Some((x, y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
|
|
||||||
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
|
|
||||||
/// [`Terminal::draw`].
|
|
||||||
pub struct CompletedFrame<'a> {
|
|
||||||
pub buffer: &'a Buffer,
|
|
||||||
pub area: Rect,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> Drop for Terminal<B>
|
|
||||||
where
|
|
||||||
B: Backend,
|
|
||||||
{
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// Attempt to restore the cursor state
|
|
||||||
if self.hidden_cursor {
|
|
||||||
if let Err(err) = self.show_cursor() {
|
|
||||||
eprintln!("Failed to show the cursor: {err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<B> Terminal<B>
|
|
||||||
where
|
|
||||||
B: Backend,
|
|
||||||
{
|
|
||||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
|
||||||
/// default colors for the foreground and the background
|
|
||||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
|
||||||
let size = backend.size()?;
|
|
||||||
Ok(Terminal::with_options(
|
|
||||||
backend,
|
|
||||||
TerminalOptions {
|
|
||||||
viewport: Viewport {
|
|
||||||
area: size,
|
|
||||||
resize_behavior: ResizeBehavior::Auto,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// UNSTABLE
|
|
||||||
pub fn with_options(backend: B, options: TerminalOptions) -> Terminal<B> {
|
|
||||||
Terminal {
|
|
||||||
backend,
|
|
||||||
buffers: [
|
|
||||||
Buffer::empty(options.viewport.area),
|
|
||||||
Buffer::empty(options.viewport.area),
|
|
||||||
],
|
|
||||||
current: 0,
|
|
||||||
hidden_cursor: false,
|
|
||||||
viewport: options.viewport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a Frame object which provides a consistent view into the terminal state for rendering.
|
|
||||||
pub fn get_frame(&mut self) -> Frame<B> {
|
|
||||||
Frame {
|
|
||||||
terminal: self,
|
|
||||||
cursor_position: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_buffer_mut(&mut self) -> &mut Buffer {
|
|
||||||
&mut self.buffers[self.current]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn backend(&self) -> &B {
|
|
||||||
&self.backend
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn backend_mut(&mut self) -> &mut B {
|
|
||||||
&mut self.backend
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtains a difference between the previous and the current buffer and passes it to the
|
|
||||||
/// current backend for drawing.
|
|
||||||
pub fn flush(&mut self) -> io::Result<()> {
|
|
||||||
let previous_buffer = &self.buffers[1 - self.current];
|
|
||||||
let current_buffer = &self.buffers[self.current];
|
|
||||||
let updates = previous_buffer.diff(current_buffer);
|
|
||||||
self.backend.draw(updates.into_iter())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
|
||||||
/// be saved so the size can remain consistent when rendering.
|
|
||||||
/// This leads to a full clear of the screen.
|
|
||||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
|
||||||
self.buffers[self.current].resize(area);
|
|
||||||
self.buffers[1 - self.current].resize(area);
|
|
||||||
self.viewport.area = area;
|
|
||||||
self.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
|
||||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
|
||||||
if self.viewport.resize_behavior == ResizeBehavior::Auto {
|
|
||||||
let size = self.size()?;
|
|
||||||
if size != self.viewport.area {
|
|
||||||
self.resize(size)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
|
||||||
/// and prepares for the next draw call.
|
|
||||||
pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Frame<B>),
|
|
||||||
{
|
|
||||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
|
||||||
// and the terminal (if growing), which may OOB.
|
|
||||||
self.autoresize()?;
|
|
||||||
|
|
||||||
let mut frame = self.get_frame();
|
|
||||||
f(&mut frame);
|
|
||||||
// We can't change the cursor position right away because we have to flush the frame to
|
|
||||||
// stdout first. But we also can't keep the frame around, since it holds a &mut to
|
|
||||||
// Terminal. Thus, we're taking the important data out of the Frame and dropping it.
|
|
||||||
let cursor_position = frame.cursor_position;
|
|
||||||
|
|
||||||
// Draw to stdout
|
|
||||||
self.flush()?;
|
|
||||||
|
|
||||||
match cursor_position {
|
|
||||||
None => self.hide_cursor()?,
|
|
||||||
Some((x, y)) => {
|
|
||||||
self.show_cursor()?;
|
|
||||||
self.set_cursor(x, y)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Swap buffers
|
|
||||||
self.buffers[1 - self.current].reset();
|
|
||||||
self.current = 1 - self.current;
|
|
||||||
|
|
||||||
// Flush
|
|
||||||
self.backend.flush()?;
|
|
||||||
Ok(CompletedFrame {
|
|
||||||
buffer: &self.buffers[1 - self.current],
|
|
||||||
area: self.viewport.area,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hide_cursor(&mut self) -> io::Result<()> {
|
|
||||||
self.backend.hide_cursor()?;
|
|
||||||
self.hidden_cursor = true;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show_cursor(&mut self) -> io::Result<()> {
|
|
||||||
self.backend.show_cursor()?;
|
|
||||||
self.hidden_cursor = false;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
|
||||||
self.backend.get_cursor()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
|
||||||
self.backend.set_cursor(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the terminal and force a full redraw on the next draw call.
|
|
||||||
pub fn clear(&mut self) -> io::Result<()> {
|
|
||||||
self.backend.clear()?;
|
|
||||||
// Reset the back buffer to make sure the next update will redraw everything.
|
|
||||||
self.buffers[1 - self.current].reset();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Queries the real size of the backend.
|
|
||||||
pub fn size(&self) -> io::Result<Rect> {
|
|
||||||
self.backend.size()
|
|
||||||
}
|
|
||||||
}
|
|
428
src/tui/text.rs
428
src/tui/text.rs
@ -1,428 +0,0 @@
|
|||||||
//! Primitives for styled text.
|
|
||||||
//!
|
|
||||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
|
||||||
//! those strings may be associated to a set of styles. `tui` has three ways to represent them:
|
|
||||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
|
||||||
//! - A single line string where each grapheme may have its own style is represented by [`Spans`].
|
|
||||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
|
||||||
//! [`Text`].
|
|
||||||
//!
|
|
||||||
//! These types form a hierarchy: [`Spans`] is a collection of [`Span`] and each line of [`Text`]
|
|
||||||
//! is a [`Spans`].
|
|
||||||
//!
|
|
||||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
|
||||||
//! supported for their properties. Moreover, `tui` provides convenient `From` implementations so
|
|
||||||
//! that you can start by using simple `String` or `&str` and then promote them to the previous
|
|
||||||
//! primitives when you need additional styling capabilities.
|
|
||||||
//!
|
|
||||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
|
||||||
//! its `title` property (which is a [`Spans`] under the hood):
|
|
||||||
//!
|
|
||||||
//! ```rust
|
|
||||||
//! # use tui::widgets::Block;
|
|
||||||
//! # use tui::text::{Span, Spans};
|
|
||||||
//! # use tui::style::{Color, Style};
|
|
||||||
//! // A simple string with no styling.
|
|
||||||
//! // Converted to Spans(vec![
|
|
||||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
|
||||||
//! // ])
|
|
||||||
//! let block = Block::default().title("My title");
|
|
||||||
//!
|
|
||||||
//! // A simple string with a unique style.
|
|
||||||
//! // Converted to Spans(vec![
|
|
||||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { fg: Some(Color::Yellow), .. }
|
|
||||||
//! // ])
|
|
||||||
//! let block = Block::default().title(
|
|
||||||
//! Span::styled("My title", Style::default().fg(Color::Yellow))
|
|
||||||
//! );
|
|
||||||
//!
|
|
||||||
//! // A string with multiple styles.
|
|
||||||
//! // Converted to Spans(vec![
|
|
||||||
//! // Span { content: Cow::Borrowed("My"), style: Style { fg: Some(Color::Yellow), .. } },
|
|
||||||
//! // Span { content: Cow::Borrowed(" title"), .. }
|
|
||||||
//! // ])
|
|
||||||
//! let block = Block::default().title(vec![
|
|
||||||
//! Span::styled("My", Style::default().fg(Color::Yellow)),
|
|
||||||
//! Span::raw(" title"),
|
|
||||||
//! ]);
|
|
||||||
//! ```
|
|
||||||
use crate::tui::style::Style;
|
|
||||||
use std::borrow::Cow;
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
/// A grapheme associated to a style.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct StyledGrapheme<'a> {
|
|
||||||
pub symbol: &'a str,
|
|
||||||
pub style: Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string where all graphemes have the same style.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Span<'a> {
|
|
||||||
pub content: Cow<'a, str>,
|
|
||||||
pub style: Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Span<'a> {
|
|
||||||
/// Create a span with no style.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::Span;
|
|
||||||
/// Span::raw("My text");
|
|
||||||
/// Span::raw(String::from("My text"));
|
|
||||||
/// ```
|
|
||||||
pub fn raw<T>(content: T) -> Span<'a>
|
|
||||||
where
|
|
||||||
T: Into<Cow<'a, str>>,
|
|
||||||
{
|
|
||||||
Span {
|
|
||||||
content: content.into(),
|
|
||||||
style: Style::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a span with a style.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::Span;
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
|
||||||
/// Span::styled("My text", style);
|
|
||||||
/// Span::styled(String::from("My text"), style);
|
|
||||||
/// ```
|
|
||||||
pub fn styled<T>(content: T, style: Style) -> Span<'a>
|
|
||||||
where
|
|
||||||
T: Into<Cow<'a, str>>,
|
|
||||||
{
|
|
||||||
Span {
|
|
||||||
content: content.into(),
|
|
||||||
style,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the width of the content held by this span.
|
|
||||||
pub fn width(&self) -> usize {
|
|
||||||
self.content.width()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an iterator over the graphemes held by this span.
|
|
||||||
///
|
|
||||||
/// `base_style` is the [`Style`] that will be patched with each grapheme [`Style`] to get
|
|
||||||
/// the resulting [`Style`].
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::{Span, StyledGrapheme};
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// # use std::iter::Iterator;
|
|
||||||
/// let style = Style::default().fg(Color::Yellow);
|
|
||||||
/// let span = Span::styled("Text", style);
|
|
||||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
|
||||||
/// let styled_graphemes = span.styled_graphemes(style);
|
|
||||||
/// assert_eq!(
|
|
||||||
/// vec![
|
|
||||||
/// StyledGrapheme {
|
|
||||||
/// symbol: "T",
|
|
||||||
/// style: Style {
|
|
||||||
/// fg: Some(Color::Yellow),
|
|
||||||
/// bg: Some(Color::Black),
|
|
||||||
/// add_modifier: Modifier::empty(),
|
|
||||||
/// sub_modifier: Modifier::empty(),
|
|
||||||
/// },
|
|
||||||
/// },
|
|
||||||
/// StyledGrapheme {
|
|
||||||
/// symbol: "e",
|
|
||||||
/// style: Style {
|
|
||||||
/// fg: Some(Color::Yellow),
|
|
||||||
/// bg: Some(Color::Black),
|
|
||||||
/// add_modifier: Modifier::empty(),
|
|
||||||
/// sub_modifier: Modifier::empty(),
|
|
||||||
/// },
|
|
||||||
/// },
|
|
||||||
/// StyledGrapheme {
|
|
||||||
/// symbol: "x",
|
|
||||||
/// style: Style {
|
|
||||||
/// fg: Some(Color::Yellow),
|
|
||||||
/// bg: Some(Color::Black),
|
|
||||||
/// add_modifier: Modifier::empty(),
|
|
||||||
/// sub_modifier: Modifier::empty(),
|
|
||||||
/// },
|
|
||||||
/// },
|
|
||||||
/// StyledGrapheme {
|
|
||||||
/// symbol: "t",
|
|
||||||
/// style: Style {
|
|
||||||
/// fg: Some(Color::Yellow),
|
|
||||||
/// bg: Some(Color::Black),
|
|
||||||
/// add_modifier: Modifier::empty(),
|
|
||||||
/// sub_modifier: Modifier::empty(),
|
|
||||||
/// },
|
|
||||||
/// },
|
|
||||||
/// ],
|
|
||||||
/// styled_graphemes.collect::<Vec<StyledGrapheme>>()
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
pub fn styled_graphemes(
|
|
||||||
&'a self,
|
|
||||||
base_style: Style,
|
|
||||||
) -> impl Iterator<Item = StyledGrapheme<'a>> {
|
|
||||||
UnicodeSegmentation::graphemes(self.content.as_ref(), true)
|
|
||||||
.map(move |g| StyledGrapheme {
|
|
||||||
symbol: g,
|
|
||||||
style: base_style.patch(self.style),
|
|
||||||
})
|
|
||||||
.filter(|s| s.symbol != "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<String> for Span<'a> {
|
|
||||||
fn from(s: String) -> Span<'a> {
|
|
||||||
Span::raw(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a str> for Span<'a> {
|
|
||||||
fn from(s: &'a str) -> Span<'a> {
|
|
||||||
Span::raw(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string composed of clusters of graphemes, each with their own style.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
|
||||||
pub struct Spans<'a>(pub Vec<Span<'a>>);
|
|
||||||
|
|
||||||
impl<'a> Spans<'a> {
|
|
||||||
/// Returns the width of the underlying string.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::{Span, Spans};
|
|
||||||
/// # use tui::style::{Color, Style};
|
|
||||||
/// let spans = Spans::from(vec![
|
|
||||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
|
||||||
/// Span::raw(" text"),
|
|
||||||
/// ]);
|
|
||||||
/// assert_eq!(7, spans.width());
|
|
||||||
/// ```
|
|
||||||
pub fn width(&self) -> usize {
|
|
||||||
self.0.iter().map(Span::width).sum()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<String> for Spans<'a> {
|
|
||||||
fn from(s: String) -> Spans<'a> {
|
|
||||||
Spans(vec![Span::from(s)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a str> for Spans<'a> {
|
|
||||||
fn from(s: &'a str) -> Spans<'a> {
|
|
||||||
Spans(vec![Span::from(s)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Vec<Span<'a>>> for Spans<'a> {
|
|
||||||
fn from(spans: Vec<Span<'a>>) -> Spans<'a> {
|
|
||||||
Spans(spans)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Span<'a>> for Spans<'a> {
|
|
||||||
fn from(span: Span<'a>) -> Spans<'a> {
|
|
||||||
Spans(vec![span])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Spans<'a>> for String {
|
|
||||||
fn from(line: Spans<'a>) -> String {
|
|
||||||
line.0.iter().fold(String::new(), |mut acc, s| {
|
|
||||||
acc.push_str(s.content.as_ref());
|
|
||||||
acc
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A string split over multiple lines where each line is composed of several clusters, each with
|
|
||||||
/// their own style.
|
|
||||||
///
|
|
||||||
/// A [`Text`], like a [`Span`], can be constructed using one of the many `From` implementations
|
|
||||||
/// or via the [`Text::raw`] and [`Text::styled`] methods. Helpfully, [`Text`] also implements
|
|
||||||
/// [`core::iter::Extend`] which enables the concatenation of several [`Text`] blocks.
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::Text;
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
|
||||||
///
|
|
||||||
/// // An initial two lines of `Text` built from a `&str`
|
|
||||||
/// let mut text = Text::from("The first line\nThe second line");
|
|
||||||
/// assert_eq!(2, text.height());
|
|
||||||
///
|
|
||||||
/// // Adding two more unstyled lines
|
|
||||||
/// text.extend(Text::raw("These are two\nmore lines!"));
|
|
||||||
/// assert_eq!(4, text.height());
|
|
||||||
///
|
|
||||||
/// // Adding a final two styled lines
|
|
||||||
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
|
|
||||||
/// assert_eq!(6, text.height());
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq, Default, Eq)]
|
|
||||||
pub struct Text<'a> {
|
|
||||||
pub lines: Vec<Spans<'a>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Text<'a> {
|
|
||||||
/// Create some text (potentially multiple lines) with no style.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::Text;
|
|
||||||
/// Text::raw("The first line\nThe second line");
|
|
||||||
/// Text::raw(String::from("The first line\nThe second line"));
|
|
||||||
/// ```
|
|
||||||
pub fn raw<T>(content: T) -> Text<'a>
|
|
||||||
where
|
|
||||||
T: Into<Cow<'a, str>>,
|
|
||||||
{
|
|
||||||
Text {
|
|
||||||
lines: match content.into() {
|
|
||||||
Cow::Borrowed(s) => s.lines().map(Spans::from).collect(),
|
|
||||||
Cow::Owned(s) => s.lines().map(|l| Spans::from(l.to_owned())).collect(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create some text (potentially multiple lines) with a style.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::Text;
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
|
||||||
/// Text::styled("The first line\nThe second line", style);
|
|
||||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
|
||||||
/// ```
|
|
||||||
pub fn styled<T>(content: T, style: Style) -> Text<'a>
|
|
||||||
where
|
|
||||||
T: Into<Cow<'a, str>>,
|
|
||||||
{
|
|
||||||
let mut text = Text::raw(content);
|
|
||||||
text.patch_style(style);
|
|
||||||
text
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the max width of all the lines.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use tui::text::Text;
|
|
||||||
/// let text = Text::from("The first line\nThe second line");
|
|
||||||
/// assert_eq!(15, text.width());
|
|
||||||
/// ```
|
|
||||||
pub fn width(&self) -> usize {
|
|
||||||
self.lines
|
|
||||||
.iter()
|
|
||||||
.map(Spans::width)
|
|
||||||
.max()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the height.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use tui::text::Text;
|
|
||||||
/// let text = Text::from("The first line\nThe second line");
|
|
||||||
/// assert_eq!(2, text.height());
|
|
||||||
/// ```
|
|
||||||
pub fn height(&self) -> usize {
|
|
||||||
self.lines.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a new style to existing text.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// # use tui::text::Text;
|
|
||||||
/// # use tui::style::{Color, Modifier, Style};
|
|
||||||
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
|
|
||||||
/// let mut raw_text = Text::raw("The first line\nThe second line");
|
|
||||||
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);
|
|
||||||
/// assert_ne!(raw_text, styled_text);
|
|
||||||
///
|
|
||||||
/// raw_text.patch_style(style);
|
|
||||||
/// assert_eq!(raw_text, styled_text);
|
|
||||||
/// ```
|
|
||||||
pub fn patch_style(&mut self, style: Style) {
|
|
||||||
for line in &mut self.lines {
|
|
||||||
for span in &mut line.0 {
|
|
||||||
span.style = span.style.patch(style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<String> for Text<'a> {
|
|
||||||
fn from(s: String) -> Text<'a> {
|
|
||||||
Text::raw(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<&'a str> for Text<'a> {
|
|
||||||
fn from(s: &'a str) -> Text<'a> {
|
|
||||||
Text::raw(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Cow<'a, str>> for Text<'a> {
|
|
||||||
fn from(s: Cow<'a, str>) -> Text<'a> {
|
|
||||||
Text::raw(s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Span<'a>> for Text<'a> {
|
|
||||||
fn from(span: Span<'a>) -> Text<'a> {
|
|
||||||
Text {
|
|
||||||
lines: vec![Spans::from(span)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Spans<'a>> for Text<'a> {
|
|
||||||
fn from(spans: Spans<'a>) -> Text<'a> {
|
|
||||||
Text { lines: vec![spans] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Vec<Spans<'a>>> for Text<'a> {
|
|
||||||
fn from(lines: Vec<Spans<'a>>) -> Text<'a> {
|
|
||||||
Text { lines }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> IntoIterator for Text<'a> {
|
|
||||||
type Item = Spans<'a>;
|
|
||||||
type IntoIter = std::vec::IntoIter<Self::Item>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.lines.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Extend<Spans<'a>> for Text<'a> {
|
|
||||||
fn extend<T: IntoIterator<Item = Spans<'a>>>(&mut self, iter: T) {
|
|
||||||
self.lines.extend(iter);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,562 +0,0 @@
|
|||||||
use crate::tui::{
|
|
||||||
buffer::Buffer,
|
|
||||||
layout::{Alignment, Rect},
|
|
||||||
style::Style,
|
|
||||||
symbols::line,
|
|
||||||
text::Spans,
|
|
||||||
widgets::{Borders, Widget},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum BorderType {
|
|
||||||
Plain,
|
|
||||||
Rounded,
|
|
||||||
Double,
|
|
||||||
Thick,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorderType {
|
|
||||||
pub fn line_symbols(border_type: BorderType) -> line::Set {
|
|
||||||
match border_type {
|
|
||||||
BorderType::Plain => line::NORMAL,
|
|
||||||
BorderType::Rounded => line::ROUNDED,
|
|
||||||
BorderType::Double => line::DOUBLE,
|
|
||||||
BorderType::Thick => line::THICK,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base widget to be used with all upper level ones. It may be used to display a box border around
|
|
||||||
/// the widget and/or add a title.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use tui::widgets::{Block, BorderType, Borders};
|
|
||||||
/// # use tui::style::{Style, Color};
|
|
||||||
/// Block::default()
|
|
||||||
/// .title("Block")
|
|
||||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
|
||||||
/// .border_style(Style::default().fg(Color::White))
|
|
||||||
/// .border_type(BorderType::Rounded)
|
|
||||||
/// .style(Style::default().bg(Color::Black));
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Block<'a> {
|
|
||||||
/// Optional title place on the upper left of the block
|
|
||||||
title: Option<Spans<'a>>,
|
|
||||||
/// Title alignment. The default is top left of the block, but one can choose to place
|
|
||||||
/// title in the top middle, or top right of the block
|
|
||||||
title_alignment: Alignment,
|
|
||||||
/// Visible borders
|
|
||||||
borders: Borders,
|
|
||||||
/// Border style
|
|
||||||
border_style: Style,
|
|
||||||
/// Type of the border. The default is plain lines but one can choose to have rounded corners
|
|
||||||
/// or doubled lines instead.
|
|
||||||
border_type: BorderType,
|
|
||||||
/// Widget style
|
|
||||||
style: Style,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Default for Block<'a> {
|
|
||||||
fn default() -> Block<'a> {
|
|
||||||
Block {
|
|
||||||
title: None,
|
|
||||||
title_alignment: Alignment::Left,
|
|
||||||
borders: Borders::NONE,
|
|
||||||
border_style: Style::default(),
|
|
||||||
border_type: BorderType::Plain,
|
|
||||||
style: Style::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Block<'a> {
|
|
||||||
pub fn title<T>(mut self, title: T) -> Block<'a>
|
|
||||||
where
|
|
||||||
T: Into<Spans<'a>>,
|
|
||||||
{
|
|
||||||
self.title = Some(title.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title_alignment(mut self, alignment: Alignment) -> Block<'a> {
|
|
||||||
self.title_alignment = alignment;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn border_style(mut self, style: Style) -> Block<'a> {
|
|
||||||
self.border_style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn style(mut self, style: Style) -> Block<'a> {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn borders(mut self, flag: Borders) -> Block<'a> {
|
|
||||||
self.borders = flag;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn border_type(mut self, border_type: BorderType) -> Block<'a> {
|
|
||||||
self.border_type = border_type;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compute the inner area of a block based on its border visibility rules.
|
|
||||||
pub fn inner(&self, area: Rect) -> Rect {
|
|
||||||
let mut inner = area;
|
|
||||||
if self.borders.intersects(Borders::LEFT) {
|
|
||||||
inner.x = inner.x.saturating_add(1).min(inner.right());
|
|
||||||
inner.width = inner.width.saturating_sub(1);
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::TOP) || self.title.is_some() {
|
|
||||||
inner.y = inner.y.saturating_add(1).min(inner.bottom());
|
|
||||||
inner.height = inner.height.saturating_sub(1);
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::RIGHT) {
|
|
||||||
inner.width = inner.width.saturating_sub(1);
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::BOTTOM) {
|
|
||||||
inner.height = inner.height.saturating_sub(1);
|
|
||||||
}
|
|
||||||
inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for Block<'a> {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
||||||
if area.area() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
buf.set_style(area, self.style);
|
|
||||||
let symbols = BorderType::line_symbols(self.border_type);
|
|
||||||
|
|
||||||
// Sides
|
|
||||||
if self.borders.intersects(Borders::LEFT) {
|
|
||||||
for y in area.top()..area.bottom() {
|
|
||||||
buf.get_mut(area.left(), y)
|
|
||||||
.set_symbol(symbols.vertical)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::TOP) {
|
|
||||||
for x in area.left()..area.right() {
|
|
||||||
buf.get_mut(x, area.top())
|
|
||||||
.set_symbol(symbols.horizontal)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::RIGHT) {
|
|
||||||
let x = area.right() - 1;
|
|
||||||
for y in area.top()..area.bottom() {
|
|
||||||
buf.get_mut(x, y)
|
|
||||||
.set_symbol(symbols.vertical)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.borders.intersects(Borders::BOTTOM) {
|
|
||||||
let y = area.bottom() - 1;
|
|
||||||
for x in area.left()..area.right() {
|
|
||||||
buf.get_mut(x, y)
|
|
||||||
.set_symbol(symbols.horizontal)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Corners
|
|
||||||
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
|
|
||||||
buf.get_mut(area.right() - 1, area.bottom() - 1)
|
|
||||||
.set_symbol(symbols.bottom_right)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
|
|
||||||
buf.get_mut(area.right() - 1, area.top())
|
|
||||||
.set_symbol(symbols.top_right)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
|
|
||||||
buf.get_mut(area.left(), area.bottom() - 1)
|
|
||||||
.set_symbol(symbols.bottom_left)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
if self.borders.contains(Borders::LEFT | Borders::TOP) {
|
|
||||||
buf.get_mut(area.left(), area.top())
|
|
||||||
.set_symbol(symbols.top_left)
|
|
||||||
.set_style(self.border_style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title
|
|
||||||
if let Some(title) = self.title {
|
|
||||||
let left_border_dx = if self.borders.intersects(Borders::LEFT) {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let right_border_dx = if self.borders.intersects(Borders::RIGHT) {
|
|
||||||
1
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let title_area_width = area
|
|
||||||
.width
|
|
||||||
.saturating_sub(left_border_dx)
|
|
||||||
.saturating_sub(right_border_dx);
|
|
||||||
|
|
||||||
let title_dx = match self.title_alignment {
|
|
||||||
Alignment::Left => left_border_dx,
|
|
||||||
Alignment::Center => area.width.saturating_sub(title.width() as u16) / 2,
|
|
||||||
Alignment::Right => area
|
|
||||||
.width
|
|
||||||
.saturating_sub(title.width() as u16)
|
|
||||||
.saturating_sub(right_border_dx),
|
|
||||||
};
|
|
||||||
|
|
||||||
let title_x = area.left() + title_dx;
|
|
||||||
let title_y = area.top();
|
|
||||||
|
|
||||||
buf.set_spans(title_x, title_y, &title, title_area_width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::tui::layout::Rect;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[allow(clippy::too_many_lines)]
|
|
||||||
fn inner_takes_into_account_the_borders() {
|
|
||||||
// No borders
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().inner(Rect::default()),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
"no borders, width=0, height=0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"no borders, width=1, height=1"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Left border
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"left, width=0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 1,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"left, width=1"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::LEFT).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 2,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 1,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"left, width=2"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Top border
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::TOP).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 0
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
"top, height=0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::TOP).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
"top, height=1"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::TOP).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 2
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"top, height=2"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Right border
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"right, width=0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"right, width=1"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::RIGHT).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 2,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"right, width=2"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Bottom border
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 0
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
"bottom, height=0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
"bottom, height=1"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::BOTTOM).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 2
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
},
|
|
||||||
"bottom, height=2"
|
|
||||||
);
|
|
||||||
|
|
||||||
// All borders
|
|
||||||
assert_eq!(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.inner(Rect::default()),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
},
|
|
||||||
"all borders, width=0, height=0"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::ALL).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 1,
|
|
||||||
height: 1
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 1,
|
|
||||||
y: 1,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
"all borders, width=1, height=1"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::ALL).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 2,
|
|
||||||
height: 2,
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 1,
|
|
||||||
y: 1,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
"all borders, width=2, height=2"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().borders(Borders::ALL).inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 3,
|
|
||||||
height: 3,
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 1,
|
|
||||||
y: 1,
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
},
|
|
||||||
"all borders, width=3, height=3"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn inner_takes_into_account_the_title() {
|
|
||||||
assert_eq!(
|
|
||||||
Block::default().title("Test").inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1,
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 1,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default()
|
|
||||||
.title("Test")
|
|
||||||
.title_alignment(Alignment::Center)
|
|
||||||
.inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1,
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 1,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Block::default()
|
|
||||||
.title("Test")
|
|
||||||
.title_alignment(Alignment::Right)
|
|
||||||
.inner(Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 1,
|
|
||||||
}),
|
|
||||||
Rect {
|
|
||||||
x: 0,
|
|
||||||
y: 1,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,159 +0,0 @@
|
|||||||
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
|
|
||||||
//!
|
|
||||||
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
|
|
||||||
//! meant to be stored but used as *commands* to draw common figures in the UI.
|
|
||||||
//!
|
|
||||||
//! The available widgets are:
|
|
||||||
//! - [`Block`]
|
|
||||||
//! - [`Paragraph`]
|
|
||||||
|
|
||||||
mod block;
|
|
||||||
mod paragraph;
|
|
||||||
mod reflow;
|
|
||||||
|
|
||||||
pub use self::block::{Block, BorderType};
|
|
||||||
pub use self::paragraph::{Paragraph, Wrap};
|
|
||||||
|
|
||||||
use crate::tui::{buffer::Buffer, layout::Rect};
|
|
||||||
use bitflags::bitflags;
|
|
||||||
|
|
||||||
bitflags! {
|
|
||||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
|
||||||
pub struct Borders: u32 {
|
|
||||||
/// Show no border (default)
|
|
||||||
const NONE = 0b0000_0001;
|
|
||||||
/// Show the top border
|
|
||||||
const TOP = 0b0000_0010;
|
|
||||||
/// Show the right border
|
|
||||||
const RIGHT = 0b0000_0100;
|
|
||||||
/// Show the bottom border
|
|
||||||
const BOTTOM = 0b000_1000;
|
|
||||||
/// Show the left border
|
|
||||||
const LEFT = 0b0001_0000;
|
|
||||||
/// Show all borders
|
|
||||||
const ALL = Self::TOP.bits | Self::RIGHT.bits | Self::BOTTOM.bits | Self::LEFT.bits;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Base requirements for a Widget
|
|
||||||
pub trait Widget {
|
|
||||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
|
||||||
/// to implement a custom widget.
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
|
|
||||||
/// between two draw calls.
|
|
||||||
///
|
|
||||||
/// Most widgets can be drawn directly based on the input parameters. However, some features may
|
|
||||||
/// require some kind of associated state to be implemented.
|
|
||||||
///
|
|
||||||
/// For example, the [`List`] widget can highlight the item currently selected. This can be
|
|
||||||
/// translated in an offset, which is the number of elements to skip in order to have the selected
|
|
||||||
/// item within the viewport currently allocated to this widget. The widget can therefore only
|
|
||||||
/// provide the following behavior: whenever the selected item is out of the viewport scroll to a
|
|
||||||
/// predefined position (making the selected item the last viewable item or the one in the middle
|
|
||||||
/// for example). Nonetheless, if the widget has access to the last computed offset then it can
|
|
||||||
/// implement a natural scrolling experience where the last offset is reused until the selected
|
|
||||||
/// item is out of the viewport.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```rust,no_run
|
|
||||||
/// # use std::io;
|
|
||||||
/// # use tui::Terminal;
|
|
||||||
/// # use tui::backend::{Backend, TestBackend};
|
|
||||||
/// # use tui::widgets::{Widget, List, ListItem, ListState};
|
|
||||||
///
|
|
||||||
/// // Let's say we have some events to display.
|
|
||||||
/// struct Events {
|
|
||||||
/// // `items` is the state managed by your application.
|
|
||||||
/// items: Vec<String>,
|
|
||||||
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
|
|
||||||
/// // item as well as the offset computed during the previous draw call (used to implement
|
|
||||||
/// // natural scrolling).
|
|
||||||
/// state: ListState
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// impl Events {
|
|
||||||
/// fn new(items: Vec<String>) -> Events {
|
|
||||||
/// Events {
|
|
||||||
/// items,
|
|
||||||
/// state: ListState::default(),
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// pub fn set_items(&mut self, items: Vec<String>) {
|
|
||||||
/// self.items = items;
|
|
||||||
/// // We reset the state as the associated items have changed. This effectively reset
|
|
||||||
/// // the selection as well as the stored offset.
|
|
||||||
/// self.state = ListState::default();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// // Select the next item. This will not be reflected until the widget is drawn in the
|
|
||||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
|
||||||
/// pub fn next(&mut self) {
|
|
||||||
/// let i = match self.state.selected() {
|
|
||||||
/// Some(i) => {
|
|
||||||
/// if i >= self.items.len() - 1 {
|
|
||||||
/// 0
|
|
||||||
/// } else {
|
|
||||||
/// i + 1
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// None => 0,
|
|
||||||
/// };
|
|
||||||
/// self.state.select(Some(i));
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// // Select the previous item. This will not be reflected until the widget is drawn in the
|
|
||||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
|
||||||
/// pub fn previous(&mut self) {
|
|
||||||
/// let i = match self.state.selected() {
|
|
||||||
/// Some(i) => {
|
|
||||||
/// if i == 0 {
|
|
||||||
/// self.items.len() - 1
|
|
||||||
/// } else {
|
|
||||||
/// i - 1
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// None => 0,
|
|
||||||
/// };
|
|
||||||
/// self.state.select(Some(i));
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
|
|
||||||
/// // sure that the stored offset is also reset.
|
|
||||||
/// pub fn unselect(&mut self) {
|
|
||||||
/// self.state.select(None);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// # let backend = TestBackend::new(5, 5);
|
|
||||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
|
||||||
///
|
|
||||||
/// let mut events = Events::new(vec![
|
|
||||||
/// String::from("Item 1"),
|
|
||||||
/// String::from("Item 2")
|
|
||||||
/// ]);
|
|
||||||
///
|
|
||||||
/// loop {
|
|
||||||
/// terminal.draw(|f| {
|
|
||||||
/// // The items managed by the application are transformed to something
|
|
||||||
/// // that is understood by tui.
|
|
||||||
/// let items: Vec<ListItem>= events.items.iter().map(|i| ListItem::new(i.as_ref())).collect();
|
|
||||||
/// // The `List` widget is then built with those items.
|
|
||||||
/// let list = List::new(items);
|
|
||||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
|
||||||
/// // effectively the only thing that we will "remember" from this draw call.
|
|
||||||
/// f.render_stateful_widget(list, f.size(), &mut events.state);
|
|
||||||
/// });
|
|
||||||
///
|
|
||||||
/// // In response to some input events or an external http request or whatever:
|
|
||||||
/// events.next();
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait StatefulWidget {
|
|
||||||
type State;
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
|
||||||
}
|
|
@ -1,194 +0,0 @@
|
|||||||
use crate::tui::{
|
|
||||||
buffer::Buffer,
|
|
||||||
layout::{Alignment, Rect},
|
|
||||||
style::Style,
|
|
||||||
text::{StyledGrapheme, Text},
|
|
||||||
widgets::{
|
|
||||||
reflow::{LineComposer, LineTruncator, WordWrapper},
|
|
||||||
Block, Widget,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use std::iter;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
|
|
||||||
match alignment {
|
|
||||||
Alignment::Center => (text_area_width / 2).saturating_sub(line_width / 2),
|
|
||||||
Alignment::Right => text_area_width.saturating_sub(line_width),
|
|
||||||
Alignment::Left => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A widget to display some text.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use tui::text::{Text, Spans, Span};
|
|
||||||
/// # use tui::widgets::{Block, Borders, Paragraph, Wrap};
|
|
||||||
/// # use tui::style::{Style, Color, Modifier};
|
|
||||||
/// # use tui::layout::{Alignment};
|
|
||||||
/// let text = vec![
|
|
||||||
/// Spans::from(vec![
|
|
||||||
/// Span::raw("First"),
|
|
||||||
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
|
|
||||||
/// Span::raw("."),
|
|
||||||
/// ]),
|
|
||||||
/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
|
|
||||||
/// ];
|
|
||||||
/// Paragraph::new(text)
|
|
||||||
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
|
|
||||||
/// .style(Style::default().fg(Color::White).bg(Color::Black))
|
|
||||||
/// .alignment(Alignment::Center)
|
|
||||||
/// .wrap(Wrap { trim: true });
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Paragraph<'a> {
|
|
||||||
/// A block to wrap the widget in
|
|
||||||
block: Option<Block<'a>>,
|
|
||||||
/// Widget style
|
|
||||||
style: Style,
|
|
||||||
/// How to wrap the text
|
|
||||||
wrap: Option<Wrap>,
|
|
||||||
/// The text to display
|
|
||||||
text: Text<'a>,
|
|
||||||
/// Scroll
|
|
||||||
scroll: (u16, u16),
|
|
||||||
/// Alignment of the text
|
|
||||||
alignment: Alignment,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes how to wrap text across lines.
|
|
||||||
///
|
|
||||||
/// ## Examples
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use tui::widgets::{Paragraph, Wrap};
|
|
||||||
/// # use tui::text::Text;
|
|
||||||
/// let bullet_points = Text::from(r#"Some indented points:
|
|
||||||
/// - First thing goes here and is long so that it wraps
|
|
||||||
/// - Here is another point that is long enough to wrap"#);
|
|
||||||
///
|
|
||||||
/// // With leading spaces trimmed (window width of 30 chars):
|
|
||||||
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
|
|
||||||
/// // Some indented points:
|
|
||||||
/// // - First thing goes here and is
|
|
||||||
/// // long so that it wraps
|
|
||||||
/// // - Here is another point that
|
|
||||||
/// // is long enough to wrap
|
|
||||||
///
|
|
||||||
/// // But without trimming, indentation is preserved:
|
|
||||||
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
|
|
||||||
/// // Some indented points:
|
|
||||||
/// // - First thing goes here
|
|
||||||
/// // and is long so that it wraps
|
|
||||||
/// // - Here is another point
|
|
||||||
/// // that is long enough to wrap
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Wrap {
|
|
||||||
/// Should leading whitespace be trimmed
|
|
||||||
pub trim: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Paragraph<'a> {
|
|
||||||
pub fn new<T>(text: T) -> Paragraph<'a>
|
|
||||||
where
|
|
||||||
T: Into<Text<'a>>,
|
|
||||||
{
|
|
||||||
Paragraph {
|
|
||||||
block: None,
|
|
||||||
style: Style::default(),
|
|
||||||
wrap: None,
|
|
||||||
text: text.into(),
|
|
||||||
scroll: (0, 0),
|
|
||||||
alignment: Alignment::Left,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn block(mut self, block: Block<'a>) -> Paragraph<'a> {
|
|
||||||
self.block = Some(block);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn style(mut self, style: Style) -> Paragraph<'a> {
|
|
||||||
self.style = style;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wrap(mut self, wrap: Wrap) -> Paragraph<'a> {
|
|
||||||
self.wrap = Some(wrap);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn scroll(mut self, offset: (u16, u16)) -> Paragraph<'a> {
|
|
||||||
self.scroll = offset;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alignment(mut self, alignment: Alignment) -> Paragraph<'a> {
|
|
||||||
self.alignment = alignment;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for Paragraph<'a> {
|
|
||||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
|
||||||
buf.set_style(area, self.style);
|
|
||||||
let text_area = self.block.take().map_or(area, |b| {
|
|
||||||
let inner_area = b.inner(area);
|
|
||||||
b.render(area, buf);
|
|
||||||
inner_area
|
|
||||||
});
|
|
||||||
|
|
||||||
if text_area.height < 1 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let style = self.style;
|
|
||||||
let mut styled = self.text.lines.iter().flat_map(|spans| {
|
|
||||||
spans
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.flat_map(|span| span.styled_graphemes(style))
|
|
||||||
// Required given the way composers work but might be refactored out if we change
|
|
||||||
// composers to operate on lines instead of a stream of graphemes.
|
|
||||||
.chain(iter::once(StyledGrapheme {
|
|
||||||
symbol: "\n",
|
|
||||||
style: self.style,
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut line_composer: Box<dyn LineComposer> = if let Some(Wrap { trim }) = self.wrap {
|
|
||||||
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
|
|
||||||
} else {
|
|
||||||
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
|
|
||||||
if self.alignment == Alignment::Left {
|
|
||||||
line_composer.set_horizontal_offset(self.scroll.1);
|
|
||||||
}
|
|
||||||
line_composer
|
|
||||||
};
|
|
||||||
let mut y = 0;
|
|
||||||
while let Some((current_line, current_line_width)) = line_composer.next_line() {
|
|
||||||
if y >= self.scroll.0 {
|
|
||||||
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
|
|
||||||
for StyledGrapheme { symbol, style } in current_line {
|
|
||||||
buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
|
|
||||||
.set_symbol(if symbol.is_empty() {
|
|
||||||
// If the symbol is empty, the last char which rendered last time will
|
|
||||||
// leave on the line. It's a quick fix.
|
|
||||||
" "
|
|
||||||
} else {
|
|
||||||
symbol
|
|
||||||
})
|
|
||||||
.set_style(*style);
|
|
||||||
x += symbol.width() as u16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
y += 1;
|
|
||||||
if y >= text_area.height + self.scroll.0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,537 +0,0 @@
|
|||||||
use crate::tui::text::StyledGrapheme;
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
const NBSP: &str = "\u{00a0}";
|
|
||||||
|
|
||||||
/// A state machine to pack styled symbols into lines.
|
|
||||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
|
||||||
/// iterators for that).
|
|
||||||
pub trait LineComposer<'a> {
|
|
||||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A state machine that wraps lines on word boundaries.
|
|
||||||
pub struct WordWrapper<'a, 'b> {
|
|
||||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
|
||||||
max_line_width: u16,
|
|
||||||
current_line: Vec<StyledGrapheme<'a>>,
|
|
||||||
next_line: Vec<StyledGrapheme<'a>>,
|
|
||||||
/// Removes the leading whitespace from lines
|
|
||||||
trim: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> WordWrapper<'a, 'b> {
|
|
||||||
pub fn new(
|
|
||||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
|
||||||
max_line_width: u16,
|
|
||||||
trim: bool,
|
|
||||||
) -> WordWrapper<'a, 'b> {
|
|
||||||
WordWrapper {
|
|
||||||
symbols,
|
|
||||||
max_line_width,
|
|
||||||
current_line: vec![],
|
|
||||||
next_line: vec![],
|
|
||||||
trim,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
|
|
||||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
|
||||||
if self.max_line_width == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
std::mem::swap(&mut self.current_line, &mut self.next_line);
|
|
||||||
self.next_line.truncate(0);
|
|
||||||
|
|
||||||
let mut current_line_width = self
|
|
||||||
.current_line
|
|
||||||
.iter()
|
|
||||||
.map(|StyledGrapheme { symbol, .. }| symbol.width() as u16)
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
let mut symbols_to_last_word_end: usize = 0;
|
|
||||||
let mut width_to_last_word_end: u16 = 0;
|
|
||||||
let mut prev_whitespace = false;
|
|
||||||
let mut symbols_exhausted = true;
|
|
||||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
|
||||||
symbols_exhausted = false;
|
|
||||||
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
|
|
||||||
|
|
||||||
// Ignore characters wider that the total max width.
|
|
||||||
if symbol.width() as u16 > self.max_line_width
|
|
||||||
// Skip leading whitespace when trim is enabled.
|
|
||||||
|| self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break on newline and discard it.
|
|
||||||
if symbol == "\n" {
|
|
||||||
if prev_whitespace {
|
|
||||||
current_line_width = width_to_last_word_end;
|
|
||||||
self.current_line.truncate(symbols_to_last_word_end);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark the previous symbol as word end.
|
|
||||||
if symbol_whitespace && !prev_whitespace {
|
|
||||||
symbols_to_last_word_end = self.current_line.len();
|
|
||||||
width_to_last_word_end = current_line_width;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current_line.push(StyledGrapheme { symbol, style });
|
|
||||||
current_line_width += symbol.width() as u16;
|
|
||||||
|
|
||||||
if current_line_width > self.max_line_width {
|
|
||||||
// If there was no word break in the text, wrap at the end of the line.
|
|
||||||
let (truncate_at, truncated_width) = if symbols_to_last_word_end == 0 {
|
|
||||||
(self.current_line.len() - 1, self.max_line_width)
|
|
||||||
} else {
|
|
||||||
(symbols_to_last_word_end, width_to_last_word_end)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Push the remainder to the next line but strip leading whitespace:
|
|
||||||
{
|
|
||||||
let remainder = &self.current_line[truncate_at..];
|
|
||||||
if let Some(remainder_nonwhite) =
|
|
||||||
remainder.iter().position(|StyledGrapheme { symbol, .. }| {
|
|
||||||
!symbol.chars().all(&char::is_whitespace)
|
|
||||||
})
|
|
||||||
{
|
|
||||||
self.next_line
|
|
||||||
.extend_from_slice(&remainder[remainder_nonwhite..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.current_line.truncate(truncate_at);
|
|
||||||
current_line_width = truncated_width;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_whitespace = symbol_whitespace;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Even if the iterator is exhausted, pass the previous remainder.
|
|
||||||
if symbols_exhausted && self.current_line.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((&self.current_line[..], current_line_width))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A state machine that truncates overhanging lines.
|
|
||||||
pub struct LineTruncator<'a, 'b> {
|
|
||||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
|
||||||
max_line_width: u16,
|
|
||||||
current_line: Vec<StyledGrapheme<'a>>,
|
|
||||||
/// Record the offet to skip render
|
|
||||||
horizontal_offset: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> LineTruncator<'a, 'b> {
|
|
||||||
pub fn new(
|
|
||||||
symbols: &'b mut dyn Iterator<Item = StyledGrapheme<'a>>,
|
|
||||||
max_line_width: u16,
|
|
||||||
) -> LineTruncator<'a, 'b> {
|
|
||||||
LineTruncator {
|
|
||||||
symbols,
|
|
||||||
max_line_width,
|
|
||||||
horizontal_offset: 0,
|
|
||||||
current_line: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
|
|
||||||
self.horizontal_offset = horizontal_offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
|
|
||||||
fn next_line(&mut self) -> Option<(&[StyledGrapheme<'a>], u16)> {
|
|
||||||
if self.max_line_width == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current_line.truncate(0);
|
|
||||||
let mut current_line_width = 0;
|
|
||||||
|
|
||||||
let mut skip_rest = false;
|
|
||||||
let mut symbols_exhausted = true;
|
|
||||||
let mut horizontal_offset = self.horizontal_offset as usize;
|
|
||||||
for StyledGrapheme { symbol, style } in &mut self.symbols {
|
|
||||||
symbols_exhausted = false;
|
|
||||||
|
|
||||||
// Ignore characters wider that the total max width.
|
|
||||||
if symbol.width() as u16 > self.max_line_width {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Break on newline and discard it.
|
|
||||||
if symbol == "\n" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_line_width + symbol.width() as u16 > self.max_line_width {
|
|
||||||
// Exhaust the remainder of the line.
|
|
||||||
skip_rest = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let symbol = if horizontal_offset == 0 {
|
|
||||||
symbol
|
|
||||||
} else {
|
|
||||||
let w = symbol.width();
|
|
||||||
if w > horizontal_offset {
|
|
||||||
let t = trim_offset(symbol, horizontal_offset);
|
|
||||||
horizontal_offset = 0;
|
|
||||||
t
|
|
||||||
} else {
|
|
||||||
horizontal_offset -= w;
|
|
||||||
""
|
|
||||||
}
|
|
||||||
};
|
|
||||||
current_line_width += symbol.width() as u16;
|
|
||||||
self.current_line.push(StyledGrapheme { symbol, style });
|
|
||||||
}
|
|
||||||
|
|
||||||
if skip_rest {
|
|
||||||
for StyledGrapheme { symbol, .. } in &mut self.symbols {
|
|
||||||
if symbol == "\n" {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if symbols_exhausted && self.current_line.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((&self.current_line[..], current_line_width))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This function will return a str slice which start at specified offset.
|
|
||||||
/// As src is a unicode str, start offset has to be calculated with each character.
|
|
||||||
fn trim_offset(src: &str, mut offset: usize) -> &str {
|
|
||||||
let mut start = 0;
|
|
||||||
for c in UnicodeSegmentation::graphemes(src, true) {
|
|
||||||
let w = c.width();
|
|
||||||
if w <= offset {
|
|
||||||
offset -= w;
|
|
||||||
start += c.len();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&src[start..]
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use crate::tui::style::Style;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
enum Composer {
|
|
||||||
WordWrapper { trim: bool },
|
|
||||||
LineTruncator,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_composer(which: Composer, text: &str, text_area_width: u16) -> (Vec<String>, Vec<u16>) {
|
|
||||||
let style = Style::default();
|
|
||||||
let mut styled =
|
|
||||||
UnicodeSegmentation::graphemes(text, true).map(|g| StyledGrapheme { symbol: g, style });
|
|
||||||
let mut composer: Box<dyn LineComposer> = match which {
|
|
||||||
Composer::WordWrapper { trim } => {
|
|
||||||
Box::new(WordWrapper::new(&mut styled, text_area_width, trim))
|
|
||||||
}
|
|
||||||
Composer::LineTruncator => Box::new(LineTruncator::new(&mut styled, text_area_width)),
|
|
||||||
};
|
|
||||||
let mut lines = vec![];
|
|
||||||
let mut widths = vec![];
|
|
||||||
while let Some((styled, width)) = composer.next_line() {
|
|
||||||
let line = styled
|
|
||||||
.iter()
|
|
||||||
.map(|StyledGrapheme { symbol, .. }| *symbol)
|
|
||||||
.collect::<String>();
|
|
||||||
assert!(width <= text_area_width);
|
|
||||||
lines.push(line);
|
|
||||||
widths.push(width);
|
|
||||||
}
|
|
||||||
(lines, widths)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_one_line() {
|
|
||||||
let width = 40;
|
|
||||||
for i in 1..width {
|
|
||||||
let text = "a".repeat(i);
|
|
||||||
let (word_wrapper, _) =
|
|
||||||
run_composer(Composer::WordWrapper { trim: true }, &text, width as u16);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width as u16);
|
|
||||||
let expected = vec![text];
|
|
||||||
assert_eq!(word_wrapper, expected);
|
|
||||||
assert_eq!(line_truncator, expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_short_lines() {
|
|
||||||
let width = 20;
|
|
||||||
let text =
|
|
||||||
"abcdefg\nhijklmno\npabcdefg\nhijklmn\nopabcdefghijk\nlmnopabcd\n\n\nefghijklmno";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
|
|
||||||
let wrapped: Vec<&str> = text.split('\n').collect();
|
|
||||||
assert_eq!(word_wrapper, wrapped);
|
|
||||||
assert_eq!(line_truncator, wrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_long_word() {
|
|
||||||
let width = 20;
|
|
||||||
let text = "abcdefghijklmnopabcdefghijklmnopabcdefghijklmnopabcdefghijklmno";
|
|
||||||
let (word_wrapper, _) =
|
|
||||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
|
||||||
|
|
||||||
let wrapped = vec![
|
|
||||||
&text[..width],
|
|
||||||
&text[width..width * 2],
|
|
||||||
&text[width * 2..width * 3],
|
|
||||||
&text[width * 3..],
|
|
||||||
];
|
|
||||||
assert_eq!(
|
|
||||||
word_wrapper, wrapped,
|
|
||||||
"WordWrapper should detect the line cannot be broken on word boundary and \
|
|
||||||
break it at line width limit."
|
|
||||||
);
|
|
||||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_long_sentence() {
|
|
||||||
let width = 20;
|
|
||||||
let text =
|
|
||||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l m n o";
|
|
||||||
let text_multi_space =
|
|
||||||
"abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab c d e f g h i j k l \
|
|
||||||
m n o";
|
|
||||||
let (word_wrapper_single_space, _) =
|
|
||||||
run_composer(Composer::WordWrapper { trim: true }, text, width as u16);
|
|
||||||
let (word_wrapper_multi_space, _) = run_composer(
|
|
||||||
Composer::WordWrapper { trim: true },
|
|
||||||
text_multi_space,
|
|
||||||
width as u16,
|
|
||||||
);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width as u16);
|
|
||||||
|
|
||||||
let word_wrapped = vec![
|
|
||||||
"abcd efghij",
|
|
||||||
"klmnopabcd efgh",
|
|
||||||
"ijklmnopabcdefg",
|
|
||||||
"hijkl mnopab c d e f",
|
|
||||||
"g h i j k l m n o",
|
|
||||||
];
|
|
||||||
assert_eq!(word_wrapper_single_space, word_wrapped);
|
|
||||||
assert_eq!(word_wrapper_multi_space, word_wrapped);
|
|
||||||
|
|
||||||
assert_eq!(line_truncator, vec![&text[..width]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_zero_width() {
|
|
||||||
let width = 0;
|
|
||||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
|
|
||||||
let expected: Vec<&str> = Vec::new();
|
|
||||||
assert_eq!(word_wrapper, expected);
|
|
||||||
assert_eq!(line_truncator, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_max_line_width_of_1() {
|
|
||||||
let width = 1;
|
|
||||||
let text = "abcd efghij klmnopabcd efgh ijklmnopabcdefg hijkl mnopab ";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
|
|
||||||
let expected: Vec<&str> = UnicodeSegmentation::graphemes(text, true)
|
|
||||||
.filter(|g| g.chars().any(|c| !c.is_whitespace()))
|
|
||||||
.collect();
|
|
||||||
assert_eq!(word_wrapper, expected);
|
|
||||||
assert_eq!(line_truncator, vec!["a"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_max_line_width_of_1_double_width_characters() {
|
|
||||||
let width = 1;
|
|
||||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字\naaaによる通信を行う場合にその\
|
|
||||||
両端点では、";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
assert_eq!(word_wrapper, vec!["", "a", "a", "a"]);
|
|
||||||
assert_eq!(line_truncator, vec!["", "a"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests `WordWrapper` with words some of which exceed line length and some not.
|
|
||||||
#[test]
|
|
||||||
fn line_composer_word_wrapper_mixed_length() {
|
|
||||||
let width = 20;
|
|
||||||
let text = "abcd efghij klmnopabcdefghijklmnopabcdefghijkl mnopab cdefghi j klmno";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
assert_eq!(
|
|
||||||
word_wrapper,
|
|
||||||
vec![
|
|
||||||
"abcd efghij",
|
|
||||||
"klmnopabcdefghijklmn",
|
|
||||||
"opabcdefghijkl",
|
|
||||||
"mnopab cdefghi j",
|
|
||||||
"klmno",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_double_width_chars() {
|
|
||||||
let width = 20;
|
|
||||||
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
|
|
||||||
では、";
|
|
||||||
let (word_wrapper, word_wrapper_width) =
|
|
||||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
|
|
||||||
let wrapped = vec![
|
|
||||||
"コンピュータ上で文字",
|
|
||||||
"を扱う場合、典型的に",
|
|
||||||
"は文字による通信を行",
|
|
||||||
"う場合にその両端点で",
|
|
||||||
"は、",
|
|
||||||
];
|
|
||||||
assert_eq!(word_wrapper, wrapped);
|
|
||||||
assert_eq!(word_wrapper_width, vec![width, width, width, width, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_leading_whitespace_removal() {
|
|
||||||
let width = 20;
|
|
||||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", "AAA",]);
|
|
||||||
assert_eq!(line_truncator, vec!["AAAAAAAAAAAAAAAAAAAA"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests truncation of leading whitespace.
|
|
||||||
#[test]
|
|
||||||
fn line_composer_lots_of_spaces() {
|
|
||||||
let width = 20;
|
|
||||||
let text = " ";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
assert_eq!(word_wrapper, vec![""]);
|
|
||||||
assert_eq!(line_truncator, vec![" "]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tests an input starting with a letter, folowed by spaces - some of the behaviour is
|
|
||||||
/// incidental.
|
|
||||||
#[test]
|
|
||||||
fn line_composer_char_plus_lots_of_spaces() {
|
|
||||||
let width = 20;
|
|
||||||
let text = "a ";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
|
|
||||||
// What's happening below is: the first line gets consumed, trailing spaces discarded,
|
|
||||||
// after 20 of which a word break occurs (probably shouldn't). The second line break
|
|
||||||
// discards all whitespace. The result should probably be vec!["a"] but it doesn't matter
|
|
||||||
// that much.
|
|
||||||
assert_eq!(word_wrapper, vec!["a", ""]);
|
|
||||||
assert_eq!(line_truncator, vec!["a "]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_word_wrapper_double_width_chars_mixed_with_spaces() {
|
|
||||||
let width = 20;
|
|
||||||
// Japanese seems not to use spaces but we should break on spaces anyway... We're using it
|
|
||||||
// to test double-width chars.
|
|
||||||
// You are more than welcome to add word boundary detection based of alterations of
|
|
||||||
// hiragana and katakana...
|
|
||||||
// This happens to also be a test case for mixed width because regular spaces are single width.
|
|
||||||
let text = "コンピュ ータ上で文字を扱う場合、 典型的には文 字による 通信を行 う場合にその両端点では、";
|
|
||||||
let (word_wrapper, word_wrapper_width) =
|
|
||||||
run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
assert_eq!(
|
|
||||||
word_wrapper,
|
|
||||||
vec![
|
|
||||||
"コンピュ",
|
|
||||||
"ータ上で文字を扱う場",
|
|
||||||
"合、 典型的には文",
|
|
||||||
"字による 通信を行",
|
|
||||||
"う場合にその両端点で",
|
|
||||||
"は、",
|
|
||||||
]
|
|
||||||
);
|
|
||||||
// Odd-sized lines have a space in them.
|
|
||||||
assert_eq!(word_wrapper_width, vec![8, 20, 17, 17, 20, 4]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Ensure words separated by nbsp are wrapped as if they were a single one.
|
|
||||||
#[test]
|
|
||||||
fn line_composer_word_wrapper_nbsp() {
|
|
||||||
let width = 20;
|
|
||||||
let text = "AAAAAAAAAAAAAAA AAAA\u{00a0}AAA";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
|
|
||||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{00a0}AAA",]);
|
|
||||||
|
|
||||||
// Ensure that if the character was a regular space, it would be wrapped differently.
|
|
||||||
let text_space = text.replace('\u{00a0}', " ");
|
|
||||||
let (word_wrapper_space, _) =
|
|
||||||
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
|
|
||||||
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_word_wrapper_preserve_indentation() {
|
|
||||||
let width = 20;
|
|
||||||
let text = "AAAAAAAAAAAAAAAAAAAA AAA";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
|
||||||
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAAAAAAA", " AAA",]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_word_wrapper_preserve_indentation_with_wrap() {
|
|
||||||
let width = 10;
|
|
||||||
let text = "AAA AAA AAAAA AA AAAAAA\n B\n C\n D";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
|
||||||
assert_eq!(
|
|
||||||
word_wrapper,
|
|
||||||
vec!["AAA AAA", "AAAAA AA", "AAAAAA", " B", " C", " D"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn line_composer_word_wrapper_preserve_indentation_lots_of_whitespace() {
|
|
||||||
let width = 10;
|
|
||||||
let text = " 4 Indent\n must wrap!";
|
|
||||||
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: false }, text, width);
|
|
||||||
assert_eq!(
|
|
||||||
word_wrapper,
|
|
||||||
vec![
|
|
||||||
" ",
|
|
||||||
" 4",
|
|
||||||
"Indent",
|
|
||||||
" ",
|
|
||||||
" must",
|
|
||||||
"wrap!"
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user