Compare commits
1 Commits
kd/assert-
...
feat-inlin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02213a25a8 |
@@ -20,6 +20,7 @@ edition = "2018"
|
|||||||
default = ["crossterm"]
|
default = ["crossterm"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
tracing = "0.1"
|
||||||
bitflags = "1.3"
|
bitflags = "1.3"
|
||||||
cassowary = "0.3"
|
cassowary = "0.3"
|
||||||
unicode-segmentation = "1.2"
|
unicode-segmentation = "1.2"
|
||||||
@@ -29,6 +30,7 @@ crossterm = { version = "0.22", optional = true }
|
|||||||
serde = { version = "1", optional = true, features = ["derive"]}
|
serde = { version = "1", optional = true, features = ["derive"]}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
tracing-subscriber = "0.2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
argh = "0.1"
|
argh = "0.1"
|
||||||
|
|
||||||
@@ -87,3 +89,7 @@ required-features = ["crossterm"]
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "user_input"
|
name = "user_input"
|
||||||
required-features = ["crossterm"]
|
required-features = ["crossterm"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "inline"
|
||||||
|
required-features = ["crossterm"]
|
||||||
|
|||||||
311
examples/inline.rs
Normal file
311
examples/inline.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
use rand::distributions::{Distribution, Uniform};
|
||||||
|
use std::{
|
||||||
|
collections::{BTreeMap, VecDeque},
|
||||||
|
error::Error,
|
||||||
|
io,
|
||||||
|
sync::mpsc,
|
||||||
|
thread,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
use tracing::{event, span, Level};
|
||||||
|
use tui::{
|
||||||
|
backend::{Backend, CrosstermBackend},
|
||||||
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
|
style::{Color, Modifier, Style},
|
||||||
|
symbols,
|
||||||
|
text::{Span, Spans},
|
||||||
|
widgets::{Block, Gauge, LineGauge, List, ListItem, Paragraph, Widget},
|
||||||
|
Frame, Terminal, TerminalOptions, ViewportVariant,
|
||||||
|
};
|
||||||
|
|
||||||
|
const NUM_DOWNLOADS: usize = 10;
|
||||||
|
|
||||||
|
type DownloadId = usize;
|
||||||
|
type WorkerId = usize;
|
||||||
|
|
||||||
|
enum Event {
|
||||||
|
Input(crossterm::event::KeyEvent),
|
||||||
|
Tick,
|
||||||
|
Resize,
|
||||||
|
DownloadUpdate(WorkerId, DownloadId, f64),
|
||||||
|
DownloadDone(WorkerId, DownloadId),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Downloads {
|
||||||
|
pending: VecDeque<Download>,
|
||||||
|
in_progress: BTreeMap<WorkerId, DownloadInProgress>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Downloads {
|
||||||
|
fn next(&mut self, worker_id: WorkerId) -> Option<Download> {
|
||||||
|
match self.pending.pop_front() {
|
||||||
|
Some(d) => {
|
||||||
|
self.in_progress.insert(
|
||||||
|
worker_id,
|
||||||
|
DownloadInProgress {
|
||||||
|
id: d.id,
|
||||||
|
started_at: Instant::now(),
|
||||||
|
progress: 0.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Some(d)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DownloadInProgress {
|
||||||
|
id: DownloadId,
|
||||||
|
started_at: Instant,
|
||||||
|
progress: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Download {
|
||||||
|
id: DownloadId,
|
||||||
|
size: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Worker {
|
||||||
|
id: WorkerId,
|
||||||
|
tx: mpsc::Sender<Download>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
|
.with_writer(io::stderr)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
|
let stdout = io::stdout();
|
||||||
|
let backend = CrosstermBackend::new(stdout);
|
||||||
|
let mut terminal = Terminal::with_options(
|
||||||
|
backend,
|
||||||
|
TerminalOptions {
|
||||||
|
viewport: ViewportVariant::Inline(8),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
input_handling(tx.clone());
|
||||||
|
let workers = workers(tx);
|
||||||
|
let mut downloads = downloads();
|
||||||
|
|
||||||
|
for w in &workers {
|
||||||
|
let d = downloads.next(w.id).unwrap();
|
||||||
|
w.tx.send(d).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
run_app(&mut terminal, workers, downloads, rx)?;
|
||||||
|
|
||||||
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
|
terminal.clear()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_handling(tx: mpsc::Sender<Event>) {
|
||||||
|
let tick_rate = Duration::from_millis(200);
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut last_tick = Instant::now();
|
||||||
|
loop {
|
||||||
|
// poll for tick rate duration, if no events, sent tick event.
|
||||||
|
let timeout = tick_rate
|
||||||
|
.checked_sub(last_tick.elapsed())
|
||||||
|
.unwrap_or_else(|| Duration::from_secs(0));
|
||||||
|
if crossterm::event::poll(timeout).unwrap() {
|
||||||
|
match crossterm::event::read().unwrap() {
|
||||||
|
crossterm::event::Event::Key(key) => tx.send(Event::Input(key)).unwrap(),
|
||||||
|
crossterm::event::Event::Resize(_, _) => tx.send(Event::Resize).unwrap(),
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if last_tick.elapsed() >= tick_rate {
|
||||||
|
tx.send(Event::Tick).unwrap();
|
||||||
|
last_tick = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn workers(tx: mpsc::Sender<Event>) -> Vec<Worker> {
|
||||||
|
(0..4)
|
||||||
|
.map(|id| {
|
||||||
|
let (worker_tx, worker_rx) = mpsc::channel::<Download>();
|
||||||
|
let tx = tx.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
while let Ok(download) = worker_rx.recv() {
|
||||||
|
let mut remaining = download.size;
|
||||||
|
while remaining > 0 {
|
||||||
|
let wait = (remaining as u64).min(10);
|
||||||
|
thread::sleep(Duration::from_millis(wait * 10));
|
||||||
|
remaining = remaining.saturating_sub(10);
|
||||||
|
let progress = (download.size - remaining) * 100 / download.size;
|
||||||
|
tx.send(Event::DownloadUpdate(id, download.id, progress as f64))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
tx.send(Event::DownloadDone(id, download.id)).unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Worker { id, tx: worker_tx }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn downloads() -> Downloads {
|
||||||
|
let distribution = Uniform::new(0, 1000);
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
let pending = (0..NUM_DOWNLOADS)
|
||||||
|
.map(|id| {
|
||||||
|
let size = distribution.sample(&mut rng);
|
||||||
|
Download { id, size }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Downloads {
|
||||||
|
pending,
|
||||||
|
in_progress: BTreeMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_app<B: Backend>(
|
||||||
|
terminal: &mut Terminal<B>,
|
||||||
|
workers: Vec<Worker>,
|
||||||
|
mut downloads: Downloads,
|
||||||
|
rx: mpsc::Receiver<Event>,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut redraw = true;
|
||||||
|
loop {
|
||||||
|
if redraw {
|
||||||
|
terminal.draw(|f| ui(f, &downloads))?;
|
||||||
|
}
|
||||||
|
redraw = true;
|
||||||
|
|
||||||
|
let span = span!(Level::INFO, "recv");
|
||||||
|
let _guard = span.enter();
|
||||||
|
match rx.recv()? {
|
||||||
|
Event::Input(event) => {
|
||||||
|
if event.code == crossterm::event::KeyCode::Char('q') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Resize => {
|
||||||
|
event!(Level::INFO, "resize");
|
||||||
|
terminal.resize()?;
|
||||||
|
}
|
||||||
|
Event::Tick => {
|
||||||
|
event!(Level::INFO, "tick");
|
||||||
|
}
|
||||||
|
Event::DownloadUpdate(worker_id, download_id, progress) => {
|
||||||
|
event!(
|
||||||
|
Level::INFO,
|
||||||
|
worker_id,
|
||||||
|
download_id,
|
||||||
|
progress,
|
||||||
|
"download update"
|
||||||
|
);
|
||||||
|
let download = downloads.in_progress.get_mut(&worker_id).unwrap();
|
||||||
|
download.progress = progress;
|
||||||
|
redraw = false
|
||||||
|
}
|
||||||
|
Event::DownloadDone(worker_id, download_id) => {
|
||||||
|
event!(Level::INFO, worker_id, download_id, "download done");
|
||||||
|
let download = downloads.in_progress.remove(&worker_id).unwrap();
|
||||||
|
terminal.insert_before(1, |buf| {
|
||||||
|
Paragraph::new(Spans::from(vec![
|
||||||
|
Span::from("Finished "),
|
||||||
|
Span::styled(
|
||||||
|
format!("download {}", download_id),
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::from(format!(
|
||||||
|
" in {}ms",
|
||||||
|
download.started_at.elapsed().as_millis()
|
||||||
|
)),
|
||||||
|
]))
|
||||||
|
.render(buf.area, buf);
|
||||||
|
})?;
|
||||||
|
match downloads.next(worker_id) {
|
||||||
|
Some(d) => workers[worker_id].tx.send(d).unwrap(),
|
||||||
|
None => {
|
||||||
|
if downloads.in_progress.is_empty() {
|
||||||
|
terminal.insert_before(1, |buf| {
|
||||||
|
Paragraph::new("Done !").render(buf.area, buf);
|
||||||
|
})?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui<B: Backend>(f: &mut Frame<B>, downloads: &Downloads) {
|
||||||
|
let size = f.size();
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title("Progress")
|
||||||
|
.title_alignment(Alignment::Center);
|
||||||
|
f.render_widget(block, size);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.constraints(vec![Constraint::Length(2), Constraint::Length(4)])
|
||||||
|
.margin(1)
|
||||||
|
.split(size);
|
||||||
|
|
||||||
|
// total progress
|
||||||
|
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||||
|
let progress = LineGauge::default()
|
||||||
|
.gauge_style(Style::default().fg(Color::Blue))
|
||||||
|
.label(format!("{}/{}", done, NUM_DOWNLOADS))
|
||||||
|
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||||
|
f.render_widget(progress, chunks[0]);
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(vec![Constraint::Percentage(20), Constraint::Percentage(80)])
|
||||||
|
.split(chunks[1]);
|
||||||
|
|
||||||
|
// in progress downloads
|
||||||
|
let items: Vec<ListItem> = downloads
|
||||||
|
.in_progress
|
||||||
|
.iter()
|
||||||
|
.map(|(_worker_id, download)| {
|
||||||
|
ListItem::new(Spans::from(vec![
|
||||||
|
Span::raw(symbols::DOT),
|
||||||
|
Span::styled(
|
||||||
|
format!(" download {:>2}", download.id),
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightGreen)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
),
|
||||||
|
Span::raw(format!(
|
||||||
|
" ({}ms)",
|
||||||
|
download.started_at.elapsed().as_millis()
|
||||||
|
)),
|
||||||
|
]))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let list = List::new(items);
|
||||||
|
f.render_widget(list, chunks[0]);
|
||||||
|
|
||||||
|
for (i, (_, download)) in downloads.in_progress.iter().enumerate() {
|
||||||
|
let gauge = Gauge::default()
|
||||||
|
.gauge_style(Style::default().fg(Color::Yellow))
|
||||||
|
.ratio(download.progress / 100.0);
|
||||||
|
if chunks[1].top().saturating_add(i as u16) > size.bottom() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
f.render_widget(
|
||||||
|
gauge,
|
||||||
|
Rect {
|
||||||
|
x: chunks[1].left(),
|
||||||
|
y: chunks[1].top().saturating_add(i as u16),
|
||||||
|
width: chunks[1].width,
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend,
|
backend::{Backend, ClearType},
|
||||||
buffer::Cell,
|
buffer::Cell,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Modifier},
|
style::{Color, Modifier},
|
||||||
@@ -11,7 +11,7 @@ use crossterm::{
|
|||||||
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
|
||||||
SetForegroundColor,
|
SetForegroundColor,
|
||||||
},
|
},
|
||||||
terminal::{self, Clear, ClearType},
|
terminal::{self, Clear},
|
||||||
};
|
};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ where
|
|||||||
for (x, y, cell) in content {
|
for (x, y, cell) in content {
|
||||||
// Move the cursor if the previous location was not (x - 1, y)
|
// 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) {
|
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
|
||||||
map_error(queue!(self.buffer, MoveTo(x, y)))?;
|
queue!(self.buffer, MoveTo(x, y))?;
|
||||||
}
|
}
|
||||||
last_pos = Some((x, y));
|
last_pos = Some((x, y));
|
||||||
if cell.modifier != modifier {
|
if cell.modifier != modifier {
|
||||||
@@ -69,45 +69,60 @@ where
|
|||||||
}
|
}
|
||||||
if cell.fg != fg {
|
if cell.fg != fg {
|
||||||
let color = CColor::from(cell.fg);
|
let color = CColor::from(cell.fg);
|
||||||
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
|
queue!(self.buffer, SetForegroundColor(color))?;
|
||||||
fg = cell.fg;
|
fg = cell.fg;
|
||||||
}
|
}
|
||||||
if cell.bg != bg {
|
if cell.bg != bg {
|
||||||
let color = CColor::from(cell.bg);
|
let color = CColor::from(cell.bg);
|
||||||
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
|
queue!(self.buffer, SetBackgroundColor(color))?;
|
||||||
bg = cell.bg;
|
bg = cell.bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
|
queue!(self.buffer, Print(&cell.symbol))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
map_error(queue!(
|
queue!(
|
||||||
self.buffer,
|
self.buffer,
|
||||||
SetForegroundColor(CColor::Reset),
|
SetForegroundColor(CColor::Reset),
|
||||||
SetBackgroundColor(CColor::Reset),
|
SetBackgroundColor(CColor::Reset),
|
||||||
SetAttribute(CAttribute::Reset)
|
SetAttribute(CAttribute::Reset)
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||||
map_error(execute!(self.buffer, Hide))
|
execute!(self.buffer, Hide)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_cursor(&mut self) -> io::Result<()> {
|
fn show_cursor(&mut self) -> io::Result<()> {
|
||||||
map_error(execute!(self.buffer, Show))
|
execute!(self.buffer, Show)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
|
||||||
crossterm::cursor::position()
|
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<()> {
|
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||||
map_error(execute!(self.buffer, MoveTo(x, y)))
|
execute!(self.buffer, MoveTo(x, y))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self) -> io::Result<()> {
|
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||||
map_error(execute!(self.buffer, Clear(ClearType::All)))
|
execute!(
|
||||||
|
self.buffer,
|
||||||
|
Clear(match clear_type {
|
||||||
|
ClearType::All => crossterm::terminal::ClearType::All,
|
||||||
|
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||||
|
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||||
|
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
|
||||||
|
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||||
|
for _ in 0..n {
|
||||||
|
queue!(self.buffer, Print("\n"))?;
|
||||||
|
}
|
||||||
|
self.buffer.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn size(&self) -> io::Result<Rect> {
|
fn size(&self) -> io::Result<Rect> {
|
||||||
@@ -122,10 +137,6 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
impl From<Color> for CColor {
|
||||||
fn from(color: Color) -> Self {
|
fn from(color: Color) -> Self {
|
||||||
match color {
|
match color {
|
||||||
@@ -166,54 +177,54 @@ impl ModifierDiff {
|
|||||||
//use crossterm::Attribute;
|
//use crossterm::Attribute;
|
||||||
let removed = self.from - self.to;
|
let removed = self.from - self.to;
|
||||||
if removed.contains(Modifier::REVERSED) {
|
if removed.contains(Modifier::REVERSED) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
|
queue!(w, SetAttribute(CAttribute::NoReverse))?;
|
||||||
}
|
}
|
||||||
if removed.contains(Modifier::BOLD) {
|
if removed.contains(Modifier::BOLD) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||||
if self.to.contains(Modifier::DIM) {
|
if self.to.contains(Modifier::DIM) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if removed.contains(Modifier::ITALIC) {
|
if removed.contains(Modifier::ITALIC) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
|
queue!(w, SetAttribute(CAttribute::NoItalic))?;
|
||||||
}
|
}
|
||||||
if removed.contains(Modifier::UNDERLINED) {
|
if removed.contains(Modifier::UNDERLINED) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
|
queue!(w, SetAttribute(CAttribute::NoUnderline))?;
|
||||||
}
|
}
|
||||||
if removed.contains(Modifier::DIM) {
|
if removed.contains(Modifier::DIM) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
|
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
|
||||||
}
|
}
|
||||||
if removed.contains(Modifier::CROSSED_OUT) {
|
if removed.contains(Modifier::CROSSED_OUT) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
|
queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
|
||||||
}
|
}
|
||||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
|
queue!(w, SetAttribute(CAttribute::NoBlink))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let added = self.to - self.from;
|
let added = self.to - self.from;
|
||||||
if added.contains(Modifier::REVERSED) {
|
if added.contains(Modifier::REVERSED) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
|
queue!(w, SetAttribute(CAttribute::Reverse))?;
|
||||||
}
|
}
|
||||||
if added.contains(Modifier::BOLD) {
|
if added.contains(Modifier::BOLD) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
|
queue!(w, SetAttribute(CAttribute::Bold))?;
|
||||||
}
|
}
|
||||||
if added.contains(Modifier::ITALIC) {
|
if added.contains(Modifier::ITALIC) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
|
queue!(w, SetAttribute(CAttribute::Italic))?;
|
||||||
}
|
}
|
||||||
if added.contains(Modifier::UNDERLINED) {
|
if added.contains(Modifier::UNDERLINED) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
|
queue!(w, SetAttribute(CAttribute::Underlined))?;
|
||||||
}
|
}
|
||||||
if added.contains(Modifier::DIM) {
|
if added.contains(Modifier::DIM) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
|
queue!(w, SetAttribute(CAttribute::Dim))?;
|
||||||
}
|
}
|
||||||
if added.contains(Modifier::CROSSED_OUT) {
|
if added.contains(Modifier::CROSSED_OUT) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
|
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
|
||||||
}
|
}
|
||||||
if added.contains(Modifier::SLOW_BLINK) {
|
if added.contains(Modifier::SLOW_BLINK) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
|
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
|
||||||
}
|
}
|
||||||
if added.contains(Modifier::RAPID_BLINK) {
|
if added.contains(Modifier::RAPID_BLINK) {
|
||||||
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
|
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -16,15 +16,25 @@ pub use self::crossterm::CrosstermBackend;
|
|||||||
mod test;
|
mod test;
|
||||||
pub use self::test::TestBackend;
|
pub use self::test::TestBackend;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum ClearType {
|
||||||
|
All,
|
||||||
|
AfterCursor,
|
||||||
|
BeforeCursor,
|
||||||
|
CurrentLine,
|
||||||
|
UntilNewLine,
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Backend {
|
pub trait Backend {
|
||||||
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error>
|
||||||
where
|
where
|
||||||
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
I: Iterator<Item = (u16, u16, &'a Cell)>;
|
||||||
|
fn append_lines(&mut self, n: u16) -> io::Result<()>;
|
||||||
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
fn hide_cursor(&mut self) -> Result<(), io::Error>;
|
||||||
fn show_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 get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
|
||||||
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
|
||||||
fn clear(&mut self) -> Result<(), io::Error>;
|
fn clear(&mut self, clear_type: ClearType) -> Result<(), io::Error>;
|
||||||
fn size(&self) -> Result<Rect, io::Error>;
|
fn size(&self) -> Result<Rect, io::Error>;
|
||||||
fn flush(&mut self) -> Result<(), io::Error>;
|
fn flush(&mut self) -> Result<(), io::Error>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use super::Backend;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
backend::{Backend, ClearType},
|
||||||
buffer::Cell,
|
buffer::Cell,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
style::{Color, Modifier},
|
style::{Color, Modifier},
|
||||||
@@ -42,10 +42,21 @@ impl<W> Backend for TermionBackend<W>
|
|||||||
where
|
where
|
||||||
W: Write,
|
W: Write,
|
||||||
{
|
{
|
||||||
/// Clears the entire screen and move the cursor to the top left of the screen
|
fn clear(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||||
fn clear(&mut self) -> io::Result<()> {
|
match clear_type {
|
||||||
write!(self.stdout, "{}", termion::clear::All)?;
|
ClearType::All => write!(self.stdout, "{}", termion::clear::All)?,
|
||||||
write!(self.stdout, "{}", termion::cursor::Goto(1, 1))?;
|
ClearType::AfterCursor => write!(self.stdout, "{}", termion::clear::AfterCursor)?,
|
||||||
|
ClearType::BeforeCursor => write!(self.stdout, "{}", termion::clear::BeforeCursor)?,
|
||||||
|
ClearType::CurrentLine => write!(self.stdout, "{}", termion::clear::CurrentLine)?,
|
||||||
|
ClearType::UntilNewLine => write!(self.stdout, "{}", termion::clear::UntilNewline)?,
|
||||||
|
};
|
||||||
|
self.stdout.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_lines(&mut self, n: u16) -> io::Result<()> {
|
||||||
|
for _ in 0..n {
|
||||||
|
writeln!(self.stdout)?;
|
||||||
|
}
|
||||||
self.stdout.flush()
|
self.stdout.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend,
|
backend::{Backend, ClearType},
|
||||||
buffer::{Buffer, Cell},
|
buffer::{Buffer, Cell},
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
};
|
};
|
||||||
@@ -117,6 +117,10 @@ impl Backend for TestBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn append_lines(&mut self, _n: u16) -> Result<(), io::Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
fn hide_cursor(&mut self) -> Result<(), io::Error> {
|
||||||
self.cursor = false;
|
self.cursor = false;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -136,7 +140,7 @@ impl Backend for TestBackend {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear(&mut self) -> Result<(), io::Error> {
|
fn clear(&mut self, _clear_type: ClearType) -> Result<(), io::Error> {
|
||||||
self.buffer.reset();
|
self.buffer.reset();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,7 +431,6 @@ impl Buffer {
|
|||||||
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
pub fn diff<'a>(&self, other: &'a Buffer) -> Vec<(u16, u16, &'a Cell)> {
|
||||||
let previous_buffer = &self.content;
|
let previous_buffer = &self.content;
|
||||||
let next_buffer = &other.content;
|
let next_buffer = &other.content;
|
||||||
let width = self.area.width;
|
|
||||||
|
|
||||||
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
let mut updates: Vec<(u16, u16, &Cell)> = vec![];
|
||||||
// Cells invalidated by drawing/replacing preceeding multi-width characters:
|
// Cells invalidated by drawing/replacing preceeding multi-width characters:
|
||||||
@@ -441,8 +440,7 @@ impl Buffer {
|
|||||||
let mut to_skip: usize = 0;
|
let mut to_skip: usize = 0;
|
||||||
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() {
|
||||||
if (current != previous || invalidated > 0) && to_skip == 0 {
|
if (current != previous || invalidated > 0) && to_skip == 0 {
|
||||||
let x = i as u16 % width;
|
let (x, y) = self.pos_of(i);
|
||||||
let y = i as u16 / width;
|
|
||||||
updates.push((x, y, &next_buffer[i]));
|
updates.push((x, y, &next_buffer[i]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -169,4 +169,4 @@ pub mod terminal;
|
|||||||
pub mod text;
|
pub mod text;
|
||||||
pub mod widgets;
|
pub mod widgets;
|
||||||
|
|
||||||
pub use self::terminal::{Frame, Terminal, TerminalOptions, Viewport};
|
pub use self::terminal::{Frame, Terminal, TerminalOptions, ViewportVariant};
|
||||||
|
|||||||
278
src/terminal.rs
278
src/terminal.rs
@@ -1,40 +1,30 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
backend::Backend,
|
backend::{Backend, ClearType},
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
widgets::{StatefulWidget, Widget},
|
widgets::{StatefulWidget, Widget},
|
||||||
};
|
};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use tracing::{event, span, Level};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
/// UNSTABLE
|
pub enum ViewportVariant {
|
||||||
enum ResizeBehavior {
|
Fullscreen,
|
||||||
Fixed,
|
Inline(u16),
|
||||||
Auto,
|
Fixed(Rect),
|
||||||
}
|
|
||||||
|
|
||||||
#[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)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
/// Options to pass to [`Terminal::with_options`]
|
/// Options to pass to [`Terminal::with_options`]
|
||||||
pub struct TerminalOptions {
|
pub struct TerminalOptions {
|
||||||
/// Viewport used to draw to the terminal
|
/// Viewport used to draw to the terminal
|
||||||
pub viewport: Viewport,
|
pub viewport: ViewportVariant,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
struct Viewport {
|
||||||
|
variant: ViewportVariant,
|
||||||
|
area: Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interface to the terminal backed by Termion
|
/// Interface to the terminal backed by Termion
|
||||||
@@ -53,6 +43,11 @@ where
|
|||||||
hidden_cursor: bool,
|
hidden_cursor: bool,
|
||||||
/// Viewport
|
/// Viewport
|
||||||
viewport: Viewport,
|
viewport: Viewport,
|
||||||
|
/// Last known size of the terminal. Used to detect if the internal buffers have to be resized.
|
||||||
|
last_known_size: Rect,
|
||||||
|
/// Last known position of the cursor. Used to find the new area when the viewport is inlined
|
||||||
|
/// and the terminal resized.
|
||||||
|
last_known_cursor_pos: (u16, u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a consistent terminal interface for rendering.
|
/// Represents a consistent terminal interface for rendering.
|
||||||
@@ -73,7 +68,7 @@ impl<'a, B> Frame<'a, B>
|
|||||||
where
|
where
|
||||||
B: Backend,
|
B: Backend,
|
||||||
{
|
{
|
||||||
/// Terminal size, guaranteed not to change when rendering.
|
/// Frame size, guaranteed not to change when rendering.
|
||||||
pub fn size(&self) -> Rect {
|
pub fn size(&self) -> Rect {
|
||||||
self.terminal.viewport.area
|
self.terminal.viewport.area
|
||||||
}
|
}
|
||||||
@@ -173,29 +168,50 @@ where
|
|||||||
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
/// Wrapper around Terminal initialization. Each buffer is initialized with a blank string and
|
||||||
/// default colors for the foreground and the background
|
/// default colors for the foreground and the background
|
||||||
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
pub fn new(backend: B) -> io::Result<Terminal<B>> {
|
||||||
let size = backend.size()?;
|
|
||||||
Terminal::with_options(
|
Terminal::with_options(
|
||||||
backend,
|
backend,
|
||||||
TerminalOptions {
|
TerminalOptions {
|
||||||
viewport: Viewport {
|
viewport: ViewportVariant::Fullscreen,
|
||||||
area: size,
|
|
||||||
resize_behavior: ResizeBehavior::Auto,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// UNSTABLE
|
pub fn with_options(mut backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
||||||
pub fn with_options(backend: B, options: TerminalOptions) -> io::Result<Terminal<B>> {
|
let size = backend.size()?;
|
||||||
|
let (viewport_area, cursor_pos) = match options.viewport {
|
||||||
|
ViewportVariant::Fullscreen => (size, (0, 0)),
|
||||||
|
ViewportVariant::Inline(height) => {
|
||||||
|
let pos = backend.get_cursor()?;
|
||||||
|
let mut row = pos.1;
|
||||||
|
let max_height = size.height.min(height);
|
||||||
|
backend.append_lines(max_height.saturating_sub(1))?;
|
||||||
|
let missing_lines = row.saturating_add(max_height).saturating_sub(size.height);
|
||||||
|
if missing_lines > 0 {
|
||||||
|
row = row.saturating_sub(missing_lines);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: row,
|
||||||
|
width: size.width,
|
||||||
|
height: max_height,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ViewportVariant::Fixed(area) => (area, (area.left(), area.top())),
|
||||||
|
};
|
||||||
Ok(Terminal {
|
Ok(Terminal {
|
||||||
backend,
|
backend,
|
||||||
buffers: [
|
buffers: [Buffer::empty(viewport_area), Buffer::empty(viewport_area)],
|
||||||
Buffer::empty(options.viewport.area),
|
|
||||||
Buffer::empty(options.viewport.area),
|
|
||||||
],
|
|
||||||
current: 0,
|
current: 0,
|
||||||
hidden_cursor: false,
|
hidden_cursor: false,
|
||||||
viewport: options.viewport,
|
viewport: Viewport {
|
||||||
|
variant: options.viewport,
|
||||||
|
area: viewport_area,
|
||||||
|
},
|
||||||
|
last_known_size: size,
|
||||||
|
last_known_cursor_pos: cursor_pos,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,28 +241,61 @@ where
|
|||||||
let previous_buffer = &self.buffers[1 - self.current];
|
let previous_buffer = &self.buffers[1 - self.current];
|
||||||
let current_buffer = &self.buffers[self.current];
|
let current_buffer = &self.buffers[self.current];
|
||||||
let updates = previous_buffer.diff(current_buffer);
|
let updates = previous_buffer.diff(current_buffer);
|
||||||
|
if let Some((col, row, _)) = updates.last() {
|
||||||
|
self.last_known_cursor_pos = (*col, *row);
|
||||||
|
}
|
||||||
self.backend.draw(updates.into_iter())
|
self.backend.draw(updates.into_iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the Terminal so that internal buffers match the requested size. Requested size will
|
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
||||||
/// be saved so the size can remain consistent when rendering.
|
pub fn resize(&mut self) -> io::Result<()> {
|
||||||
/// This leads to a full clear of the screen.
|
let size = self.size()?;
|
||||||
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
|
if self.last_known_size == size {
|
||||||
self.buffers[self.current].resize(area);
|
return Ok(());
|
||||||
self.buffers[1 - self.current].resize(area);
|
}
|
||||||
self.viewport.area = area;
|
|
||||||
self.clear()
|
event!(Level::DEBUG, last_known_size = ?self.last_known_size, ?size, "terminal size changed");
|
||||||
|
|
||||||
|
let next_area = match self.viewport.variant {
|
||||||
|
ViewportVariant::Fullscreen => size,
|
||||||
|
ViewportVariant::Inline(height) => {
|
||||||
|
let (_, mut row) = self.get_cursor()?;
|
||||||
|
let offset_in_previous_viewport = self
|
||||||
|
.last_known_cursor_pos
|
||||||
|
.1
|
||||||
|
.saturating_sub(self.viewport.area.top());
|
||||||
|
let max_height = height.min(size.height);
|
||||||
|
let lines_after_cursor = height
|
||||||
|
.saturating_sub(offset_in_previous_viewport)
|
||||||
|
.saturating_sub(1);
|
||||||
|
let available_lines = size.height.saturating_sub(row).saturating_sub(1);
|
||||||
|
let missing_lines = lines_after_cursor.saturating_sub(available_lines);
|
||||||
|
self.backend.append_lines(lines_after_cursor)?;
|
||||||
|
if missing_lines > 0 {
|
||||||
|
row = row.saturating_sub(missing_lines);
|
||||||
|
}
|
||||||
|
row = row.saturating_sub(offset_in_previous_viewport);
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: row,
|
||||||
|
width: size.width,
|
||||||
|
height: max_height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ViewportVariant::Fixed(area) => area,
|
||||||
|
};
|
||||||
|
self.set_viewport_area(next_area);
|
||||||
|
self.clear()?;
|
||||||
|
|
||||||
|
self.last_known_size = size;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queries the backend for size and resizes if it doesn't match the previous size.
|
fn set_viewport_area(&mut self, area: Rect) {
|
||||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
self.viewport.area = area;
|
||||||
if self.viewport.resize_behavior == ResizeBehavior::Auto {
|
self.buffers[self.current].resize(area);
|
||||||
let size = self.size()?;
|
self.buffers[1 - self.current].resize(area);
|
||||||
if size != self.viewport.area {
|
event!(Level::DEBUG, area = ?area, "viewport changed");
|
||||||
self.resize(size)?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
|
||||||
@@ -255,9 +304,12 @@ where
|
|||||||
where
|
where
|
||||||
F: FnOnce(&mut Frame<B>),
|
F: FnOnce(&mut Frame<B>),
|
||||||
{
|
{
|
||||||
|
let span = span!(Level::DEBUG, "draw");
|
||||||
|
let _guard = span.enter();
|
||||||
|
|
||||||
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
// Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
|
||||||
// and the terminal (if growing), which may OOB.
|
// and the terminal (if growing), which may OOB.
|
||||||
self.autoresize()?;
|
self.resize()?;
|
||||||
|
|
||||||
let mut frame = self.get_frame();
|
let mut frame = self.get_frame();
|
||||||
f(&mut frame);
|
f(&mut frame);
|
||||||
@@ -283,9 +335,12 @@ where
|
|||||||
|
|
||||||
// Flush
|
// Flush
|
||||||
self.backend.flush()?;
|
self.backend.flush()?;
|
||||||
|
|
||||||
|
event!(Level::DEBUG, "completed frame");
|
||||||
|
|
||||||
Ok(CompletedFrame {
|
Ok(CompletedFrame {
|
||||||
buffer: &self.buffers[1 - self.current],
|
buffer: &self.buffers[1 - self.current],
|
||||||
area: self.viewport.area,
|
area: self.last_known_size,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -306,12 +361,28 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
pub fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
|
||||||
self.backend.set_cursor(x, y)
|
self.backend.set_cursor(x, y)?;
|
||||||
|
self.last_known_cursor_pos = (x, y);
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the terminal and force a full redraw on the next draw call.
|
/// Clear the terminal and force a full redraw on the next draw call.
|
||||||
pub fn clear(&mut self) -> io::Result<()> {
|
pub fn clear(&mut self) -> io::Result<()> {
|
||||||
self.backend.clear()?;
|
event!(Level::DEBUG, "clear");
|
||||||
|
match self.viewport.variant {
|
||||||
|
ViewportVariant::Fullscreen => self.backend.clear(ClearType::All)?,
|
||||||
|
ViewportVariant::Inline(_) => {
|
||||||
|
self.backend
|
||||||
|
.set_cursor(self.viewport.area.left(), self.viewport.area.top())?;
|
||||||
|
self.backend.clear(ClearType::AfterCursor)?;
|
||||||
|
}
|
||||||
|
ViewportVariant::Fixed(area) => {
|
||||||
|
for row in area.top()..area.bottom() {
|
||||||
|
self.backend.set_cursor(0, row)?;
|
||||||
|
self.backend.clear(ClearType::AfterCursor)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// Reset the back buffer to make sure the next update will redraw everything.
|
// Reset the back buffer to make sure the next update will redraw everything.
|
||||||
self.buffers[1 - self.current].reset();
|
self.buffers[1 - self.current].reset();
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -321,4 +392,97 @@ where
|
|||||||
pub fn size(&self) -> io::Result<Rect> {
|
pub fn size(&self) -> io::Result<Rect> {
|
||||||
self.backend.size()
|
self.backend.size()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Insert some content before the current inline viewport. This has no effect when the
|
||||||
|
/// viewport is fullscreen.
|
||||||
|
///
|
||||||
|
/// This function scrolls down the current viewport by the given height. The newly freed space is
|
||||||
|
/// then made available to the `draw_fn` closure through a writable `Buffer`.
|
||||||
|
///
|
||||||
|
/// Before:
|
||||||
|
/// ```ignore
|
||||||
|
/// +-------------------+
|
||||||
|
/// | |
|
||||||
|
/// | viewport |
|
||||||
|
/// | |
|
||||||
|
/// +-------------------+
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// After:
|
||||||
|
/// ```ignore
|
||||||
|
/// +-------------------+
|
||||||
|
/// | buffer |
|
||||||
|
/// +-------------------+
|
||||||
|
/// +-------------------+
|
||||||
|
/// | |
|
||||||
|
/// | viewport |
|
||||||
|
/// | |
|
||||||
|
/// +-------------------+
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ## Insert a single line before the current viewport
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use tui::widgets::{Paragraph, Widget};
|
||||||
|
/// # use tui::text::{Spans, Span};
|
||||||
|
/// # use tui::style::{Color, Style};
|
||||||
|
/// # use tui::{Terminal};
|
||||||
|
/// # use tui::backend::TestBackend;
|
||||||
|
/// # let backend = TestBackend::new(10, 10);
|
||||||
|
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||||
|
/// terminal.insert_before(1, |buf| {
|
||||||
|
/// Paragraph::new(Spans::from(vec![
|
||||||
|
/// Span::raw("This line will be added "),
|
||||||
|
/// Span::styled("before", Style::default().fg(Color::Blue)),
|
||||||
|
/// Span::raw(" the current viewport")
|
||||||
|
/// ])).render(buf.area, buf);
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
pub fn insert_before<F>(&mut self, height: u16, draw_fn: F) -> io::Result<()>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Buffer),
|
||||||
|
{
|
||||||
|
let span = span!(Level::DEBUG, "insert_before");
|
||||||
|
let _guard = span.enter();
|
||||||
|
if !matches!(self.viewport.variant, ViewportVariant::Inline(_)) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.clear()?;
|
||||||
|
let height = height.min(self.last_known_size.height);
|
||||||
|
self.backend.append_lines(height)?;
|
||||||
|
let missing_lines =
|
||||||
|
height.saturating_sub(self.last_known_size.bottom() - self.viewport.area.top());
|
||||||
|
let area = Rect {
|
||||||
|
x: self.viewport.area.left(),
|
||||||
|
y: self.viewport.area.top().saturating_sub(missing_lines),
|
||||||
|
width: self.viewport.area.width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
let mut buffer = Buffer::empty(area);
|
||||||
|
|
||||||
|
draw_fn(&mut buffer);
|
||||||
|
|
||||||
|
let iter = buffer.content.iter().enumerate().map(|(i, c)| {
|
||||||
|
let (x, y) = buffer.pos_of(i);
|
||||||
|
(x, y, c)
|
||||||
|
});
|
||||||
|
self.backend.draw(iter)?;
|
||||||
|
self.backend.flush()?;
|
||||||
|
|
||||||
|
let remaining_lines = self.last_known_size.height - area.bottom();
|
||||||
|
let missing_lines = self.viewport.area.height.saturating_sub(remaining_lines);
|
||||||
|
self.backend.append_lines(self.viewport.area.height)?;
|
||||||
|
|
||||||
|
self.set_viewport_area(Rect {
|
||||||
|
x: area.left(),
|
||||||
|
y: area.bottom().saturating_sub(missing_lines),
|
||||||
|
width: area.width,
|
||||||
|
height: self.viewport.area.height,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ fn backend_termion_should_only_write_diffs() -> Result<(), Box<dyn std::error::E
|
|||||||
{
|
{
|
||||||
use tui::{
|
use tui::{
|
||||||
backend::TermionBackend, layout::Rect, widgets::Paragraph, Terminal, TerminalOptions,
|
backend::TermionBackend, layout::Rect, widgets::Paragraph, Terminal, TerminalOptions,
|
||||||
Viewport,
|
ViewportVariant,
|
||||||
};
|
};
|
||||||
let backend = TermionBackend::new(&mut stdout);
|
let backend = TermionBackend::new(&mut stdout);
|
||||||
let area = Rect::new(0, 0, 3, 1);
|
let area = Rect::new(0, 0, 3, 1);
|
||||||
let mut terminal = Terminal::with_options(
|
let mut terminal = Terminal::with_options(
|
||||||
backend,
|
backend,
|
||||||
TerminalOptions {
|
TerminalOptions {
|
||||||
viewport: Viewport::fixed(area),
|
viewport: ViewportVariant::Fixed(area),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
terminal.draw(|f| {
|
terminal.draw(|f| {
|
||||||
|
|||||||
Reference in New Issue
Block a user