Compare commits

..

6 Commits

Author SHA1 Message Date
Josh McKinney
b88717b65f docs(constraint): add note about percentages (#1368)
Co-authored-by: Orhun Parmaksız <orhun@archlinux.org>
2024-09-13 14:42:52 +03:00
Vitalii Kryvenko
5635b930c7 ci: add cargo-machete and remove unused deps (#1362)
https://github.com/bnjbvr/cargo-machete
2024-09-08 18:54:33 -07:00
Hossein Nedaee
870bc6a64a docs: use Frame::area() instead of size() in examples (#1361)
`Frame::size()` is deprecated
2024-09-08 13:20:59 -07:00
FujiApple
da821b431e fix: clippy lints from rust 1.81.0 (#1356) 2024-09-06 22:54:36 -07:00
Patryk Wychowaniec
68886d1787 fix: add unstable-backend-writer feature (#1352)
https://github.com/ratatui/ratatui/pull/991 created a new unstable
feature, but forgot to add it to Cargo.toml, making it impossible to use
on newer versions of rustc - this commit fixes it.
2024-09-06 15:33:14 -07:00
Patryk Wychowaniec
0f48239778 fix(terminal): resize() now resizes fixed viewports (#1353)
`Terminal::resize()` on a fixed viewport used to do nothing due to
an accidentally shadowed variable. This now works as intended.
2024-09-04 12:28:30 -07:00
21 changed files with 91 additions and 582 deletions

View File

@@ -42,6 +42,13 @@ jobs:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
cargo-machete:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: bnjbvr/cargo-machete@v0.6.2
clippy:
runs-on: ubuntu-latest
steps:

View File

@@ -30,19 +30,15 @@ document-features = { version = "0.2.7", optional = true }
instability = "0.3.1"
itertools = "0.13"
lru = "0.12.0"
once_cell = "1.19.0"
paste = "1.0.2"
palette = { version = "0.7.6", optional = true }
serde = { version = "1", optional = true, features = ["derive"] }
strum = { version = "0.26", features = ["derive"] }
strum_macros = { version = "0.26.3" }
strum = { version = "0.26.3", features = ["derive"] }
termwiz = { version = "0.22.0", optional = true }
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
unicode-segmentation = "1.10"
unicode-truncate = "1"
unicode-width = "0.1.13"
metrics = { version = "0.23.0", git = "https://github.com/joshka/metrics.git", branch = "jm/derive-debug" }
quanta = "0.12.3"
[target.'cfg(not(windows))'.dependencies]
# termion is not supported on Windows
@@ -53,12 +49,10 @@ argh = "0.1.12"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
crossterm = { version = "0.28.1", features = ["event-stream"] }
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
futures = "0.3.30"
indoc = "2"
metrics-util = { version = "0.17.0", git = "https://github.com/joshka/metrics.git", branch = "jm/derive-debug" }
octocrab = "0.39.0"
pretty_assertions = "1.4.0"
rand = "0.8.5"
@@ -161,7 +155,11 @@ underline-color = ["dep:crossterm"]
#! The following features are unstable and may change in the future:
## Enable all unstable features.
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"]
unstable = [
"unstable-rendered-line-info",
"unstable-widget-ref",
"unstable-backend-writer",
]
## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count)
## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods
@@ -173,6 +171,9 @@ unstable-rendered-line-info = []
## the future.
unstable-widget-ref = []
## Enables getting access to backends' writers.
unstable-backend-writer = []
[package.metadata.docs.rs]
all-features = true
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
@@ -297,11 +298,6 @@ name = "line_gauge"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "metrics"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "hyperlink"
required-features = ["crossterm"]

View File

@@ -4,14 +4,18 @@
- [Ratatui](#ratatui)
- [Installation](#installation)
- [Introduction](#introduction)
- [Other Documentation](#other-documentation)
- [Other documentation](#other-documentation)
- [Quickstart](#quickstart)
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
- [Drawing the UI](#drawing-the-ui)
- [Handling events](#handling-events)
- [Example](#example)
- [Layout](#layout)
- [Text and styling](#text-and-styling)
- [Status of this fork](#status-of-this-fork)
- [Rust version requirements](#rust-version-requirements)
- [Widgets](#widgets)
- [Built in](#built-in)
- [Third\-party libraries, bootstrapping templates and
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Third-party libraries, bootstrapping templates and widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
- [Apps](#apps)
- [Alternatives](#alternatives)
- [Acknowledgments](#acknowledgments)
@@ -170,7 +174,7 @@ fn handle_events() -> io::Result<bool> {
fn ui(frame: &mut Frame) {
frame.render_widget(
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
frame.size(),
frame.area(),
);
}
```
@@ -199,7 +203,7 @@ fn ui(frame: &mut Frame) {
Constraint::Min(0),
Constraint::Length(1),
])
.areas(frame.size());
.areas(frame.area());
let [left_area, right_area] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
.areas(main_area);
@@ -237,7 +241,7 @@ use ratatui::{
};
fn ui(frame: &mut Frame) {
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
let line = Line::from(vec![
Span::raw("Hello "),

View File

@@ -122,7 +122,7 @@ pub struct TabsState<'a> {
}
impl<'a> TabsState<'a> {
pub fn new(titles: Vec<&'a str>) -> Self {
pub const fn new(titles: Vec<&'a str>) -> Self {
Self { titles, index: 0 }
}
pub fn next(&mut self) {

View File

@@ -1,227 +0,0 @@
use std::{
sync::{atomic::Ordering, Arc},
time::{Duration, Instant},
};
use color_eyre::Result;
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use itertools::Itertools;
use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
use metrics_util::{
registry::{AtomicStorage, Registry},
Summary,
};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},
style::{palette::tailwind::SLATE, Stylize},
widgets::{Row, Table, Widget},
DefaultTerminal, Frame,
};
fn main() -> Result<()> {
color_eyre::install()?;
let recorder = MetricsRecorder::new();
let recorder_widget = recorder.widget();
recorder.install();
let terminal = ratatui::init();
let app = App::new(recorder_widget);
let result = app.run(terminal);
ratatui::restore();
result
}
#[derive(Debug)]
struct App {
should_quit: bool,
recorder_widget: RecorderWidget,
}
impl App {
const fn new(recorder_widget: RecorderWidget) -> Self {
Self {
should_quit: false,
recorder_widget,
}
}
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
let mut last_frame = Instant::now();
let frame_duration = Duration::from_secs_f64(1.0 / 60.0);
while !self.should_quit {
if last_frame.elapsed() >= frame_duration {
last_frame = Instant::now();
terminal.draw(|frame| self.draw(frame))?;
}
self.handle_events(frame_duration.saturating_sub(last_frame.elapsed()))?;
}
Ok(())
}
fn draw(&self, frame: &mut Frame) {
let [top, main] =
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(frame.area());
let title = if cfg!(debug_assertions) {
"Metrics Example (debug)"
} else {
"Metrics Example (release)"
};
frame.render_widget(title.blue().into_centered_line(), top);
frame.render_widget(&self.recorder_widget, main);
}
fn handle_events(&mut self, timeout: Duration) -> Result<()> {
if !event::poll(timeout)? {
return Ok(());
}
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_press(key),
_ => {}
}
Ok(())
}
fn on_key_press(&mut self, key: event::KeyEvent) {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
_ => {}
}
}
}
#[derive(Debug, Default)]
struct MetricsRecorder {
metrics: Arc<Metrics>,
}
impl MetricsRecorder {
fn new() -> Self {
Self::default()
}
fn widget(&self) -> RecorderWidget {
RecorderWidget {
metrics: Arc::clone(&self.metrics),
}
}
fn install(self) {
metrics::set_global_recorder(self).unwrap();
}
}
#[derive(Debug)]
struct Metrics {
registry: Registry<Key, AtomicStorage>,
}
impl Default for Metrics {
fn default() -> Self {
Self {
registry: Registry::atomic(),
}
}
}
impl Metrics {
fn counter(&self, key: &Key) -> Counter {
self.registry
.get_or_create_counter(key, |c| Counter::from_arc(c.clone()))
}
fn gauge(&self, key: &Key) -> Gauge {
self.registry
.get_or_create_gauge(key, |g| Gauge::from_arc(g.clone()))
}
fn histogram(&self, key: &Key) -> Histogram {
self.registry
.get_or_create_histogram(key, |h| Histogram::from_arc(h.clone()))
}
}
#[derive(Debug)]
struct RecorderWidget {
metrics: Arc<Metrics>,
}
impl Widget for &RecorderWidget {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let mut counters = vec![];
self.metrics.registry.visit_counters(|key, counter| {
let value = counter.load(Ordering::SeqCst);
counters.push((key.clone(), value.to_string()));
});
let mut gauges = vec![];
self.metrics.registry.visit_gauges(|key, gauge| {
let value = gauge.load(Ordering::SeqCst);
gauges.push((key.clone(), value.to_string()));
});
let mut histograms = vec![];
self.metrics.registry.visit_histograms(|key, histogram| {
let mut summary = Summary::with_defaults();
for data in histogram.data() {
summary.add(data);
}
if summary.is_empty() {
// we omit the empty histograms, but this is how you would render them
// histograms.push((key.clone(), "empty".to_string()));
} else {
let min = Duration::from_secs_f64(summary.min());
let max = Duration::from_secs_f64(summary.max());
let p50 = Duration::from_secs_f64(summary.quantile(0.5).unwrap());
let p90 = Duration::from_secs_f64(summary.quantile(0.9).unwrap());
let p99 = Duration::from_secs_f64(summary.quantile(0.99).unwrap());
let line = format!(
"min:{min:>9.2?} p50:{p50:>9.2?} p90:{p90:>9.2?} p99:{p99:>9.2?} max:{max:>9.2?}"
);
histograms.push((key.clone(), line));
}
});
counters.sort();
gauges.sort();
histograms.sort();
let lines = counters
.iter()
.chain(gauges.iter())
.chain(histograms.iter());
let row_colors = [SLATE.c950, SLATE.c900];
let rows = lines
.map(|(key, line)| Row::new([key.name(), line]))
.zip(row_colors.iter().cycle())
.map(|(row, style)| row.bg(*style))
.collect_vec();
Table::new(rows, [Constraint::Length(40), Constraint::Fill(1)]).render(area, buf);
}
}
#[allow(unused_variables)]
impl Recorder for MetricsRecorder {
fn describe_counter(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
// todo!()
}
fn describe_gauge(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
// todo!()
}
fn describe_histogram(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
// todo!()
}
fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
self.metrics.counter(key)
}
fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
self.metrics.gauge(key)
}
fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
self.metrics.histogram(key)
}
}

View File

@@ -102,13 +102,10 @@
//! [Ratatui Website]: https://ratatui.rs
use std::io;
use metrics::{Counter, Histogram};
use once_cell::sync::Lazy;
use strum::{Display, EnumString};
use crate::{
buffer::Cell,
counter, duration_histogram,
layout::{Position, Size},
};
@@ -130,89 +127,6 @@ pub use self::termwiz::TermwizBackend;
mod test;
pub use self::test::TestBackend;
static METRICS: Lazy<Metrics> = Lazy::new(Metrics::new);
#[derive(Debug)]
struct Metrics {
pub clear_region_count: Counter,
pub clear_region_duration: Histogram,
pub draw_count: Counter,
pub draw_duration: Histogram,
pub append_lines_count: Counter,
pub append_lines_duration: Histogram,
pub hide_cursor_duration: Histogram,
pub show_cursor_duration: Histogram,
pub get_cursor_position_duration: Histogram,
pub set_cursor_position_duration: Histogram,
pub size_duration: Histogram,
pub window_size_duration: Histogram,
pub flush_count: Counter,
pub flush_duration: Histogram,
}
impl Metrics {
fn new() -> Self {
Self {
clear_region_count: counter!(
"ratatui.backend.clear_region.count",
"Number of times a region of the backend buffer was cleared"
),
clear_region_duration: duration_histogram!(
"ratatui.backend.clear.time",
"Time spent clearing the backend buffer"
),
draw_count: counter!(
"ratatui.backend.draw.count",
"Number of times the backend buffer was drawn to the terminal"
),
draw_duration: duration_histogram!(
"ratatui.backend.draw.time",
"Time spent drawing the backend buffer to the terminal"
),
hide_cursor_duration: duration_histogram!(
"ratatui.backend.hide_cursor.time",
"Time spent hiding the cursor in the backend"
),
show_cursor_duration: duration_histogram!(
"ratatui.backend.show_cursor.time",
"Time spent showing the cursor in the backend"
),
get_cursor_position_duration: duration_histogram!(
"ratatui.backend.get_cursor_position.time",
"Time spent getting the cursor position from the backend"
),
set_cursor_position_duration: duration_histogram!(
"ratatui.backend.set_cursor_position.time",
"Time spent setting the cursor position in the backend"
),
append_lines_count: counter!(
"ratatui.backend.append_lines.count",
"Number of times lines were appended to the backend buffer"
),
append_lines_duration: duration_histogram!(
"ratatui.backend.append_lines.time",
"Time spent appending lines to the backend buffer"
),
size_duration: duration_histogram!(
"ratatui.backend.size.time",
"Time spent getting the size of the backend buffer"
),
window_size_duration: duration_histogram!(
"ratatui.backend.window_size.time",
"Time spent getting the window size of the backend buffer"
),
flush_count: counter!(
"ratatui.backend.flush.count",
"Number of times the backend buffer was flushed to the terminal"
),
flush_duration: duration_histogram!(
"ratatui.backend.flush.time",
"Time spent flushing the backend buffer to the terminal"
),
}
}
}
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]

View File

@@ -4,11 +4,9 @@
//! [Crossterm]: https://crates.io/crates/crossterm
use std::io::{self, Write};
use crossterm::cursor;
#[cfg(feature = "underline-color")]
use crossterm::style::SetUnderlineColor;
use super::METRICS;
use crate::{
backend::{Backend, ClearType, WindowSize},
buffer::Cell,
@@ -22,7 +20,6 @@ use crate::{
terminal::{self, Clear},
},
layout::{Position, Size},
metrics::HistogramExt,
style::{Color, Modifier, Style},
};
@@ -157,9 +154,6 @@ where
where
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
METRICS.draw_count.increment(1);
let _timing = METRICS.draw_duration.start_timing();
let mut fg = Color::Reset;
let mut bg = Color::Reset;
#[cfg(feature = "underline-color")]
@@ -216,24 +210,20 @@ where
}
fn hide_cursor(&mut self) -> io::Result<()> {
let _timing = METRICS.hide_cursor_duration.start_timing();
execute!(self.writer, Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
let _timing = METRICS.show_cursor_duration.start_timing();
execute!(self.writer, Show)
}
fn get_cursor_position(&mut self) -> io::Result<Position> {
let _timing = METRICS.get_cursor_position_duration.start_timing();
cursor::position()
crossterm::cursor::position()
.map(|(x, y)| Position { x, y })
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
let _timing = METRICS.set_cursor_position_duration.start_timing();
let Position { x, y } = position.into();
execute!(self.writer, MoveTo(x, y))
}
@@ -243,8 +233,6 @@ where
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
METRICS.clear_region_count.increment(1);
let _timing = METRICS.clear_region_duration.start_timing();
execute!(
self.writer,
Clear(match clear_type {
@@ -258,8 +246,6 @@ where
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
METRICS.append_lines_count.increment(1);
let _timing = METRICS.append_lines_duration.start_timing();
for _ in 0..n {
queue!(self.writer, Print("\n"))?;
}
@@ -267,37 +253,31 @@ where
}
fn size(&self) -> io::Result<Size> {
let _timing = METRICS.size_duration.start_timing();
terminal::size().map(Size::from)
let (width, height) = terminal::size()?;
Ok(Size { width, height })
}
fn window_size(&mut self) -> io::Result<WindowSize> {
let _timing = METRICS.window_size_duration.start_timing();
terminal::window_size().map(WindowSize::from)
let crossterm::terminal::WindowSize {
columns,
rows,
width,
height,
} = terminal::window_size()?;
Ok(WindowSize {
columns_rows: Size {
width: columns,
height: rows,
},
pixels: Size { width, height },
})
}
fn flush(&mut self) -> io::Result<()> {
METRICS.flush_count.increment(1);
let _timing = METRICS.flush_duration.start_timing();
self.writer.flush()
}
}
impl From<crossterm::terminal::WindowSize> for WindowSize {
fn from(value: crossterm::terminal::WindowSize) -> Self {
Self {
columns_rows: Size {
width: value.columns,
height: value.rows,
},
pixels: Size {
width: value.width,
height: value.height,
},
}
}
}
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {

View File

@@ -3,14 +3,10 @@ use std::{
ops::{Index, IndexMut},
};
use metrics::Histogram;
use once_cell::sync::Lazy;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::{
buffer::Cell, duration_histogram, layout::Position, metrics::HistogramExt, prelude::*,
};
use crate::{buffer::Cell, layout::Position, prelude::*};
/// A buffer that maps to the desired content of the terminal after the draw call
///
@@ -74,23 +70,6 @@ pub struct Buffer {
pub content: Vec<Cell>,
}
static METRICS: Lazy<Metrics> = Lazy::new(Metrics::new);
struct Metrics {
diff_duration: Histogram,
}
impl Metrics {
fn new() -> Self {
Self {
diff_duration: duration_histogram!(
"ratatui.buffer.diff.time",
"Time spent diffing buffers"
),
}
}
}
impl Buffer {
/// Returns a Buffer with all cells set to the default one
#[must_use]
@@ -485,7 +464,6 @@ impl Buffer {
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
/// ```
pub fn diff<'a>(&self, other: &'a Self) -> Vec<(u16, u16, &'a Cell)> {
let _timing = METRICS.diff_duration.start_timing();
let previous_buffer = &self.content;
let next_buffer = &other.content;

View File

@@ -116,8 +116,12 @@ pub enum Constraint {
/// Applies a percentage of the available space to the element
///
/// Converts the given percentage to a floating-point value and multiplies that with area.
/// This value is rounded back to a integer as part of the layout split calculation.
/// Converts the given percentage to a floating-point value and multiplies that with area. This
/// value is rounded back to a integer as part of the layout split calculation.
///
/// **Note**: As this value only accepts a `u16`, certain percentages that cannot be
/// represented exactly (e.g. 1/3) are not possible. You might want to use
/// [`Constraint::Ratio`] or [`Constraint::Fill`] in such cases.
///
/// # Examples
///

View File

@@ -428,7 +428,7 @@ impl Layout {
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let area = frame.area();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = layout.areas(area);
///
@@ -460,7 +460,7 @@ impl Layout {
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let area = frame.area();
/// let layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
/// let [top, main] = layout.areas(area);
/// let [before, inbetween, after] = layout.spacers(area);

View File

@@ -236,7 +236,7 @@ impl Rect {
/// ```rust
/// # use ratatui::prelude::*;
/// # fn render(frame: &mut Frame) {
/// let area = frame.size();
/// let area = frame.area();
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
/// # }
/// ```

View File

@@ -151,7 +151,7 @@
//! Constraint::Min(0),
//! Constraint::Length(1),
//! ])
//! .areas(frame.size());
//! .areas(frame.area());
//! let [left_area, right_area] =
//! Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
//! .areas(main_area);
@@ -189,7 +189,7 @@
//! };
//!
//! fn draw(frame: &mut Frame) {
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
//! let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
//!
//! let line = Line::from(vec![
//! Span::raw("Hello "),
@@ -310,7 +310,6 @@ pub use termwiz;
pub mod backend;
pub mod buffer;
pub mod layout;
pub(crate) mod metrics;
pub mod prelude;
pub mod style;
pub mod symbols;

View File

@@ -1,66 +0,0 @@
use metrics::Histogram;
/// A helper macro that registers and describes a counter
#[macro_export]
macro_rules! counter {
($name:expr, $description:expr $(,)?) => {{
::metrics::describe_counter!($name, ::metrics::Unit::Count, $description);
::metrics::counter!($name)
}};
}
/// A helper macro that registers and describes a histogram that tracks durations.
#[macro_export]
macro_rules! duration_histogram {
($name:expr, $description:expr $(,)?) => {{
::metrics::describe_histogram!($name, ::metrics::Unit::Seconds, $description);
::metrics::histogram!($name)
}};
}
/// A helper macro that registers and describes a histogram that tracks bytes.
#[macro_export]
macro_rules! bytes_histogram {
($name:expr, $description:expr $(,)?) => {{
::metrics::describe_histogram!($name, ::metrics::Unit::Bytes, $description);
::metrics::histogram!($name)
}};
}
pub(crate) trait HistogramExt {
fn measure_duration<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R;
fn start_timing(&self) -> DurationMeasurementGuard;
}
impl HistogramExt for Histogram {
fn measure_duration<F, R>(&self, f: F) -> R
where
F: FnOnce() -> R,
{
let start = quanta::Instant::now();
let result = f();
self.record(start.elapsed().as_secs_f64());
result
}
fn start_timing(&self) -> DurationMeasurementGuard {
DurationMeasurementGuard {
start: quanta::Instant::now(),
histogram: self.clone(), // this is safe because `Histogram` stores an `Arc`
}
}
}
pub struct DurationMeasurementGuard {
start: quanta::Instant,
histogram: Histogram,
}
impl Drop for DurationMeasurementGuard {
fn drop(&mut self) {
self.histogram.record(self.start.elapsed().as_secs_f64());
}
}

View File

@@ -18,7 +18,7 @@
//! let backend = CrosstermBackend::new(stdout());
//! let mut terminal = Terminal::new(backend)?;
//! terminal.draw(|frame| {
//! let area = frame.size();
//! let area = frame.area();
//! frame.render_widget(Paragraph::new("Hello world!"), area);
//! })?;
//! # std::io::Result::Ok(())

View File

@@ -1,11 +1,7 @@
use std::io;
use metrics::{Counter, Histogram};
use once_cell::sync::Lazy;
use crate::{
backend::ClearType, buffer::Cell, counter, duration_histogram, metrics::HistogramExt,
prelude::*, CompletedFrame, TerminalOptions, Viewport,
backend::ClearType, buffer::Cell, prelude::*, CompletedFrame, TerminalOptions, Viewport,
};
/// An interface to interact and draw [`Frame`]s on the user's terminal.
@@ -42,7 +38,7 @@ use crate::{
/// let backend = CrosstermBackend::new(stdout());
/// let mut terminal = Terminal::new(backend)?;
/// terminal.draw(|frame| {
/// let area = frame.size();
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// })?;
/// # std::io::Result::Ok(())
@@ -88,69 +84,6 @@ pub struct Options {
pub viewport: Viewport,
}
static METRICS: Lazy<Metrics> = Lazy::new(Metrics::new);
#[derive(Debug)]
struct Metrics {
pub clear_duration: Histogram,
pub draw_callback_duration: Histogram,
pub draw_count: Counter,
pub draw_duration: Histogram,
pub flush_duration: Histogram,
pub flush_count: Counter,
pub resize_duration: Histogram,
pub resize_count: Counter,
pub insert_before_count: Counter,
pub insert_before_duration: Histogram,
}
impl Metrics {
fn new() -> Self {
Self {
clear_duration: duration_histogram!(
"ratatui.terminal.clear.time",
"Time spent clearing the terminal buffer"
),
draw_callback_duration: duration_histogram!(
"ratatui.terminal.draw.callback.time",
"Time spent calling the draw callback (application code)"
),
draw_count: counter!(
"ratatui.terminal.draw.count",
"Number of times the terminal buffer was drawn to the backend"
),
draw_duration: duration_histogram!(
"ratatui.terminal.draw.time",
"Time spent drawing the terminal buffer to the backend"
),
flush_duration: duration_histogram!(
"ratatui.terminal.flush.time",
"Time spent flushing the terminal buffer to the terminal"
),
flush_count: counter!(
"ratatui.terminal.flush.count",
"Number of times the terminal buffer was flushed to the terminal"
),
resize_duration: duration_histogram!(
"ratatui.terminal.resize.time",
"Time spent resizing the terminal buffer"
),
resize_count: counter!(
"ratatui.terminal.resize.count",
"Number of times the terminal buffer was resized"
),
insert_before_count: counter!(
"ratatui.terminal.insert_before.count",
"Number of times content was inserted before the viewport"
),
insert_before_duration: duration_histogram!(
"ratatui.terminal.insert_before.time",
"Time spent inserting content before the viewport"
),
}
}
}
impl<B> Drop for Terminal<B>
where
B: Backend,
@@ -257,8 +190,6 @@ where
/// 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<()> {
METRICS.flush_count.increment(1);
let _timing = METRICS.flush_duration.start_timing();
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff(current_buffer);
@@ -273,10 +204,7 @@ where
/// Requested area will be saved to remain consistent when rendering. This leads to a full clear
/// of the screen.
pub fn resize(&mut self, area: Rect) -> io::Result<()> {
METRICS.resize_count.increment(1);
let _timing = METRICS.resize_duration.start_timing();
let next_area = match self.viewport {
Viewport::Fullscreen => area,
Viewport::Inline(height) => {
let offset_in_previous_viewport = self
.last_known_cursor_pos
@@ -290,7 +218,7 @@ where
)?
.0
}
Viewport::Fixed(area) => area,
Viewport::Fixed(_) | Viewport::Fullscreen => area,
};
self.set_viewport_area(next_area);
self.clear()?;
@@ -355,7 +283,7 @@ where
///
/// // with a closure
/// terminal.draw(|frame| {
/// let area = frame.size();
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// })?;
@@ -364,7 +292,7 @@ where
/// terminal.draw(render)?;
///
/// fn render(frame: &mut ratatui::Frame) {
/// frame.render_widget(Paragraph::new("Hello World!"), frame.size());
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// }
/// # std::io::Result::Ok(())
/// ```
@@ -427,7 +355,7 @@ where
/// // with a closure
/// terminal.try_draw(|frame| {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// let area = frame.size();
/// let area = frame.area();
/// frame.render_widget(Paragraph::new("Hello World!"), area);
/// frame.set_cursor_position(Position { x: 0, y: 0 });
/// io::Result::Ok(())
@@ -438,7 +366,7 @@ where
///
/// fn render(frame: &mut ratatui::Frame) -> io::Result<()> {
/// let value: u8 = "not a number".parse().map_err(io::Error::other)?;
/// frame.render_widget(Paragraph::new("Hello World!"), frame.size());
/// frame.render_widget(Paragraph::new("Hello World!"), frame.area());
/// Ok(())
/// }
/// # io::Result::Ok(())
@@ -448,17 +376,13 @@ where
F: FnOnce(&mut Frame) -> Result<(), E>,
E: Into<io::Error>,
{
METRICS.draw_count.increment(1);
let _timing = METRICS.draw_duration.start_timing();
// Autoresize - otherwise we get glitches if shrinking or potential desync between
// widgets and the terminal (if growing), which may OOB.
// 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();
METRICS
.draw_callback_duration
.measure_duration(|| render_callback(&mut frame).map_err(Into::into))?;
render_callback(&mut frame).map_err(Into::into)?;
// 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
@@ -540,7 +464,6 @@ where
/// Clear the terminal and force a full redraw on the next draw call.
pub fn clear(&mut self) -> io::Result<()> {
let _timing = METRICS.clear_duration.start_timing();
match self.viewport {
Viewport::Fullscreen => self.backend.clear_region(ClearType::All)?,
Viewport::Inline(_) => {
@@ -649,11 +572,9 @@ where
return Ok(());
}
METRICS.insert_before_count.increment(1);
let _timing = METRICS.insert_before_duration.start_timing();
// The approach of this function is to first render all of the lines to insert into a
// temporary buffer, and then to loop drawing chunks from the buffer to the screen.
// drawing this buffer onto the screen.
// temporary buffer, and then to loop drawing chunks from the buffer to the screen. drawing
// this buffer onto the screen.
let area = Rect {
x: 0,
y: 0,
@@ -672,9 +593,9 @@ where
let screen_height: i32 = self.last_known_area.height.into();
// The algorithm here is to loop, drawing large chunks of text (up to a screen-full at a
// time), until the remainder of the buffer plus the viewport fits on the screen. We
// choose this loop condition because it guarantees that we can write the
// remainder of the buffer with just one call to Self::draw_lines().
// time), until the remainder of the buffer plus the viewport fits on the screen. We choose
// this loop condition because it guarantees that we can write the remainder of the buffer
// with just one call to Self::draw_lines().
while buffer_height + viewport_height > screen_height {
// We will draw as much of the buffer as possible on this iteration in order to make
// forward progress. So we have:
@@ -682,9 +603,8 @@ where
// to_draw = min(buffer_height, screen_height)
//
// We may need to scroll the screen up to make room to draw. We choose the minimal
// possible scroll amount so we don't end up with the viewport sitting in the middle
// of the screen when this function is done. The amount to scroll by
// is:
// possible scroll amount so we don't end up with the viewport sitting in the middle of
// the screen when this function is done. The amount to scroll by is:
//
// scroll_up = max(0, drawn_height + to_draw - screen_height)
//
@@ -702,14 +622,13 @@ where
// There is now enough room on the screen for the remaining buffer plus the viewport,
// though we may still need to scroll up some of the existing text first. It's possible
// that by this point we've drained the buffer, but we may still need to scroll up to
// make room for the viewport.
// that by this point we've drained the buffer, but we may still need to scroll up to make
// room for the viewport.
//
// We want to scroll up the exact amount that will leave us completely filling the
// screen. However, it's possible that the viewport didn't start on the
// bottom of the screen and the added lines weren't enough to push it all
// the way to the bottom. We deal with this case by just ensuring that our
// scroll amount is non-negative.
// We want to scroll up the exact amount that will leave us completely filling the screen.
// However, it's possible that the viewport didn't start on the bottom of the screen and
// the added lines weren't enough to push it all the way to the bottom. We deal with this
// case by just ensuring that our scroll amount is non-negative.
//
// We want:
// screen_height = drawn_height - scroll_up + buffer_height + viewport_height
@@ -731,9 +650,8 @@ where
// Clear the viewport off the screen. We didn't clear earlier for two reasons. First, it
// wasn't necessary because the buffer we drew out of isn't sparse, so it overwrote
// whatever was on the screen. Second, there is a weird bug with tmux where a full
// screen clear plus immediate scrolling causes some garbage to go into the
// scrollback.
// whatever was on the screen. Second, there is a weird bug with tmux where a full screen
// clear plus immediate scrolling causes some garbage to go into the scrollback.
self.clear()?;
Ok(())

View File

@@ -81,7 +81,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
/// use ratatui::prelude::*;
///
/// # fn render_frame(frame: &mut Frame) {
/// frame.render_widget("test content".green().on_yellow().italic(), frame.size());
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
/// # }
/// ```
/// [`Line`]: crate::text::Line

View File

@@ -88,7 +88,7 @@ use crate::{buffer::Buffer, layout::Rect, style::Style};
/// # let mut terminal = Terminal::new(backend).unwrap();
///
/// terminal.draw(|frame| {
/// frame.render_widget(Clear, frame.size());
/// frame.render_widget(Clear, frame.area());
/// });
/// ```
///

View File

@@ -595,7 +595,7 @@ impl<'a> Block<'a> {
/// let outer_block = Block::bordered().title("Outer");
/// let inner_block = Block::bordered().title("Inner");
///
/// let outer_area = frame.size();
/// let outer_area = frame.area();
/// let inner_area = outer_block.inner(outer_area);
///
/// frame.render_widget(outer_block, outer_area);

View File

@@ -51,7 +51,7 @@ where
O: Iterator<Item = (I, Alignment)>,
I: Iterator<Item = StyledGrapheme<'a>>,
{
pub fn new(lines: O, max_line_width: u16, trim: bool) -> Self {
pub const fn new(lines: O, max_line_width: u16, trim: bool) -> Self {
Self {
input_lines: lines,
max_line_width,
@@ -250,7 +250,7 @@ where
O: Iterator<Item = (I, Alignment)>,
I: Iterator<Item = StyledGrapheme<'a>>,
{
pub fn new(lines: O, max_line_width: u16) -> Self {
pub const fn new(lines: O, max_line_width: u16) -> Self {
Self {
input_lines: lines,
max_line_width,

View File

@@ -59,7 +59,7 @@ use crate::{
///
/// let mut scrollbar_state = ScrollbarState::new(items.len()).position(vertical_scroll);
///
/// let area = frame.size();
/// let area = frame.area();
/// // Note we render the paragraph
/// frame.render_widget(paragraph, area);
/// // and the scrollbar, those are separate widgets

View File

@@ -237,7 +237,7 @@ mod tests {
// Helper function to render a sparkline to a buffer with a given width
// filled with x symbols to make it easier to assert on the result
fn render(widget: Sparkline, width: u16) -> Buffer {
fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
let area = Rect::new(0, 0, width, 1);
let mut buffer = Buffer::filled(area, Cell::new("x"));
widget.render(area, &mut buffer);
@@ -253,6 +253,8 @@ mod tests {
#[test]
fn it_does_not_panic_if_max_is_set_to_zero() {
// see https://github.com/rust-lang/rust-clippy/issues/13191
#[allow(clippy::unnecessary_min_or_max)]
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
let buffer = render(widget, 6);
assert_eq!(buffer, Buffer::with_lines([" xxx"]));