Compare commits

..

5 Commits

Author SHA1 Message Date
Josh McKinney
9ea840d87f feat(layout): accept (x, y) tuple for Rect::offset
This allows callers to call `Rect::offset((x, y))`` instead of the more
verbose `Rect::offset(Offset { x, y })`.
2024-03-28 16:22:43 -07:00
Josh McKinney
26af65043e feat(text): add push methods for text and line (#998)
Adds the following methods to the `Text` and `Line` structs:
- Text::push_line
- Text::push_span
- Line::push_span

This allows for adding lines and spans to a text object without having
to call methods on the fields directly, which is usefult for incremental
construction of text objects.
2024-03-28 15:30:21 -07:00
Benjamin Nickolls
07da90a718 chore(funding): add eth address for receiving funds from drips.network (#994) 2024-03-27 18:11:26 +03:00
Orhun Parmaksız
125ee929ee chore(docs): fix typos in crate documentation (#1002) 2024-03-27 16:28:23 +03:00
Josh McKinney
742a5ead06 fix(text): fix panic when rendering out of bounds (#997)
Previously it was possible to cause a panic when rendering to an area
outside of the buffer bounds. Instead this now correctly renders nothing
to the buffer.
2024-03-24 22:06:31 -07:00
9 changed files with 262 additions and 69 deletions

7
FUNDING.json Normal file
View File

@@ -0,0 +1,7 @@
{
"drips": {
"ethereum": {
"ownedBy": "0x6053C8984f4F214Ad12c4653F28514E1E09213B5"
}
}
}

View File

@@ -40,6 +40,16 @@ pub struct Offset {
pub y: i32,
}
impl<X: Into<i32>, Y: Into<i32>> From<(X, Y)> for Offset {
/// Creates a new `Offset` from a tuple of (x, y).
fn from((x, y): (X, Y)) -> Self {
Self {
x: x.into(),
y: y.into(),
}
}
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
@@ -135,9 +145,20 @@ impl Rect {
/// - Positive `x` moves the whole `Rect` to the right, negative to the left.
/// - Positive `y` moves the whole `Rect` to the bottom, negative to the top.
///
/// See [`Offset`] for details.
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, layout::Offset};
/// let rect = Rect::new(1, 2, 3, 4);
/// let rect = rect.offset(Offset { x: 10, y: 20 });
/// assert_eq!(rect, Rect::new(11, 22, 3, 4));
///
/// // offset can also be called with a tuple of (x, y)
/// let rect = rect.offset((10, 20));
/// ```
#[must_use = "method returns the modified value"]
pub fn offset(self, offset: Offset) -> Self {
pub fn offset<T: Into<Offset>>(self, offset: T) -> Self {
let offset = offset.into();
Self {
x: i32::from(self.x)
.saturating_add(offset.x)
@@ -423,6 +444,11 @@ mod tests {
);
}
#[test]
fn offset_from_tuple() {
assert_eq!(Rect::new(1, 2, 3, 4).offset((5, 6)), Rect::new(6, 8, 3, 4));
}
#[test]
fn union() {
assert_eq!(

View File

@@ -48,6 +48,7 @@ use crate::prelude::*;
/// - [`Line::reset_style`] resets the style of the line.
/// - [`Line::width`] returns the unicode width of the content held by this line.
/// - [`Line::styled_graphemes`] returns an iterator over the graphemes held by this line.
/// - [`Line::push_span`] adds a span to the line.
///
/// # Compatibility Notes
///
@@ -451,6 +452,23 @@ impl<'a> Line<'a> {
pub fn iter_mut(&mut self) -> std::slice::IterMut<Span<'a>> {
self.spans.iter_mut()
}
/// Adds a span to the line.
///
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
/// `&str`, a `String`, or a `Span`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut line = Line::from("Hello, ");
/// line.push_span(Span::raw("world!"));
/// line.push_span(" How are you?");
/// ```
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
self.spans.push(span.into());
}
}
impl<'a> IntoIterator for Line<'a> {
@@ -587,6 +605,11 @@ mod tests {
use super::*;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn raw_str() {
let line = Line::raw("test content");
@@ -825,6 +848,35 @@ mod tests {
assert_eq!(format!("{line_from_styled_span}"), "Hello, world!");
}
#[test]
fn left_aligned() {
let line = Line::from("Hello, world!").left_aligned();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let line = Line::from("Hello, world!").centered();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let line = Line::from("Hello, world!").right_aligned();
assert_eq!(line.alignment, Some(Alignment::Right));
}
#[test]
pub fn push_span() {
let mut line = Line::from("A");
line.push_span(Span::raw("B"));
line.push_span("C");
assert_eq!(
line.spans,
vec![Span::raw("A"), Span::raw("B"), Span::raw("C")]
);
}
mod widget {
use super::*;
use crate::assert_buffer_eq;
@@ -832,6 +884,7 @@ mod tests {
const GREEN: Style = Style::new().fg(Color::Green);
const ITALIC: Style = Style::new().add_modifier(Modifier::ITALIC);
#[fixture]
fn hello_world() -> Line<'static> {
Line::from(vec![
Span::styled("Hello ", BLUE),
@@ -851,6 +904,13 @@ mod tests {
assert_buffer_eq!(buf, expected);
}
#[rstest]
fn render_out_of_bounds(hello_world: Line<'static>, mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
hello_world.render(out_of_bounds, &mut small_buf);
assert_buffer_eq!(small_buf, Buffer::empty(small_buf.area));
}
#[test]
fn render_only_styles_line_area() {
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 1));
@@ -895,24 +955,6 @@ mod tests {
}
}
#[test]
fn left_aligned() {
let line = Line::from("Hello, world!").left_aligned();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let line = Line::from("Hello, world!").centered();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let line = Line::from("Hello, world!").right_aligned();
assert_eq!(line.alignment, Some(Alignment::Right));
}
mod iterators {
use super::*;

View File

@@ -363,6 +363,7 @@ impl Widget for Span<'_> {
impl WidgetRef for Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
let Rect {
x: mut current_x,
y,
@@ -402,8 +403,15 @@ impl std::fmt::Display for Span<'_> {
#[cfg(test)]
mod tests {
use rstest::fixture;
use super::*;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn default() {
let span = Span::default();
@@ -532,7 +540,30 @@ mod tests {
assert_eq!(format!("{stylized_span}"), "stylized test content");
}
#[test]
fn left_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_left_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_centered_line();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_right_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Right));
}
mod widget {
use rstest::rstest;
use super::*;
use crate::assert_buffer_eq;
@@ -550,6 +581,13 @@ mod tests {
assert_buffer_eq!(buf, expected);
}
#[rstest]
fn render_out_of_bounds(mut small_buf: Buffer) {
let out_of_bounds = Rect::new(20, 20, 10, 1);
Span::raw("Hello, World!").render(out_of_bounds, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
/// When the content of the span is longer than the area passed to render, the content
/// should be truncated
#[test]
@@ -632,25 +670,4 @@ mod tests {
assert_buffer_eq!(buf, expected);
}
}
#[test]
fn left_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_left_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_centered_line();
assert_eq!(line.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let span = Span::styled("Test Content", Style::new().green().italic());
let line = span.into_right_aligned_line();
assert_eq!(line.alignment, Some(Alignment::Right));
}
}

View File

@@ -50,6 +50,8 @@ use crate::prelude::*;
/// - [`Text::height`] returns the height.
/// - [`Text::patch_style`] patches the style of this `Text`, adding modifiers from the given style.
/// - [`Text::reset_style`] resets the style of the `Text`.
/// - [`Text::push_line`] adds a line to the text.
/// - [`Text::push_span`] adds a span to the last line of the text.
///
/// # Examples
///
@@ -441,6 +443,46 @@ impl<'a> Text<'a> {
pub fn iter_mut(&mut self) -> std::slice::IterMut<Line<'a>> {
self.lines.iter_mut()
}
/// Adds a line to the text.
///
/// `line` can be any type that can be converted into a `Line`. For example, you can pass a
/// `&str`, a `String`, a `Span`, or a `Line`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut text = Text::from("Hello, world!");
/// text.push_line(Line::from("How are you?"));
/// text.push_line(Span::from("How are you?"));
/// text.push_line("How are you?");
/// ```
pub fn push_line<T: Into<Line<'a>>>(&mut self, line: T) {
self.lines.push(line.into());
}
/// Adds a span to the last line of the text.
///
/// `span` can be any type that is convertible into a `Span`. For example, you can pass a
/// `&str`, a `String`, or a `Span`.
///
/// # Examples
///
/// ```rust
/// # use ratatui::prelude::*;
/// let mut text = Text::from("Hello, world!");
/// text.push_span(Span::from("How are you?"));
/// text.push_span("How are you?");
/// ```
pub fn push_span<T: Into<Span<'a>>>(&mut self, span: T) {
let span = span.into();
if let Some(last) = self.lines.last_mut() {
last.push_span(span);
} else {
self.lines.push(Line::from(span));
}
}
}
impl<'a> IntoIterator for Text<'a> {
@@ -559,6 +601,7 @@ impl Widget for Text<'_> {
impl WidgetRef for Text<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area);
buf.set_style(area, self.style);
for (line, row) in self.iter().zip(area.rows()) {
let line_width = line.width() as u16;
@@ -601,6 +644,11 @@ mod tests {
use super::*;
#[fixture]
fn small_buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 1))
}
#[test]
fn raw() {
let text = Text::raw("The first line\nThe second line");
@@ -848,6 +896,70 @@ mod tests {
assert_eq!(Text::default().italic().style, Modifier::ITALIC.into());
}
#[test]
fn left_aligned() {
let text = Text::from("Hello, world!").left_aligned();
assert_eq!(text.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let text = Text::from("Hello, world!").centered();
assert_eq!(text.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let text = Text::from("Hello, world!").right_aligned();
assert_eq!(text.alignment, Some(Alignment::Right));
}
#[test]
fn push_line() {
let mut text = Text::from("A");
text.push_line(Line::from("B"));
text.push_line(Span::from("C"));
text.push_line("D");
assert_eq!(
text.lines,
vec![
Line::raw("A"),
Line::raw("B"),
Line::raw("C"),
Line::raw("D")
]
);
}
#[test]
fn push_line_empty() {
let mut text = Text::default();
text.push_line(Line::from("Hello, world!"));
assert_eq!(text.lines, vec![Line::from("Hello, world!")]);
}
#[test]
fn push_span() {
let mut text = Text::from("A");
text.push_span(Span::raw("B"));
text.push_span("C");
assert_eq!(
text.lines,
vec![Line::from(vec![
Span::raw("A"),
Span::raw("B"),
Span::raw("C")
])],
);
}
#[test]
fn push_span_empty() {
let mut text = Text::default();
text.push_span(Span::raw("Hello, world!"));
assert_eq!(text.lines, vec![Line::from(Span::raw("Hello, world!"))],);
}
mod widget {
use super::*;
use crate::assert_buffer_eq;
@@ -865,6 +977,13 @@ mod tests {
assert_buffer_eq!(buf, expected_buf);
}
#[rstest]
fn render_out_of_bounds(mut small_buf: Buffer) {
let out_of_bounds_area = Rect::new(20, 20, 10, 1);
Text::from("Hello, world!").render(out_of_bounds_area, &mut small_buf);
assert_eq!(small_buf, Buffer::empty(small_buf.area));
}
#[test]
fn render_right_aligned() {
let text = Text::from("foo").alignment(Alignment::Right);
@@ -945,24 +1064,6 @@ mod tests {
}
}
#[test]
fn left_aligned() {
let text = Text::from("Hello, world!").left_aligned();
assert_eq!(text.alignment, Some(Alignment::Left));
}
#[test]
fn centered() {
let text = Text::from("Hello, world!").centered();
assert_eq!(text.alignment, Some(Alignment::Center));
}
#[test]
fn right_aligned() {
let text = Text::from("Hello, world!").right_aligned();
assert_eq!(text.alignment, Some(Alignment::Right));
}
mod iterators {
use super::*;

View File

@@ -355,7 +355,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful
/// widgets. Implemetors should prefer to implement this over the `StatefulWidget` trait and add an
/// widgets. Implementors should prefer to implement this over the `StatefulWidget` trait and add an
/// implementation of `StatefulWidget` that calls `StatefulWidgetRef::render_ref` where backwards
/// compatibility is required.
///

View File

@@ -2,8 +2,8 @@
//!
//!
//!
//! The [`Monthly`] widget will display a calendar for the monh provided in `display_date`. Days are
//! styled using the default style unless:
//! The [`Monthly`] widget will display a calendar for the month provided in `display_date`. Days
//! are styled using the default style unless:
//! * `show_surrounding` is set, then days not in the `display_date` month will use that style.
//! * a style is returned by the [`DateStyler`] for the day
//!

View File

@@ -330,7 +330,7 @@ impl<'a> Dataset<'a> {
/// Sets the data points of this dataset
///
/// Points will then either be rendered as scrattered points or with lines between them
/// Points will then either be rendered as scattered points or with lines between them
/// depending on [`Dataset::graph_type`].
///
/// Data consist in an array of `f64` tuples (`(f64, f64)`), the first element being X and the
@@ -493,7 +493,7 @@ pub struct Chart<'a> {
style: Style,
/// Constraints used to determine whether the legend should be shown or not
hidden_legend_constraints: (Constraint, Constraint),
/// The position detnermine where the legenth is shown or hide regaurdless of
/// The position determine where the length is shown or hide regardless of
/// `hidden_legend_constraints`
legend_position: Option<LegendPosition>,
}
@@ -636,7 +636,7 @@ impl<'a> Chart<'a> {
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
/// ```
///
/// Always hide the legend. Note this can be accomplished more exclicitely by passing `None` to
/// Always hide the legend. Note this can be accomplished more explicitly by passing `None` to
/// [`Chart::legend_position`].
///
/// ```

View File

@@ -643,7 +643,7 @@ impl<'a> List<'a> {
/// - [`HighlightSpacing::Always`] will always allocate the spacing, regardless of whether an
/// item is selected or not. This means that the table will never change size, regardless of
/// if an item is selected or not.
/// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if an itemis selected.
/// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if an item is selected.
/// This means that the table will shift when an item is selected. This is the default setting
/// for backwards compatibility, but it is recommended to use `HighlightSpacing::Always` for a
/// better user experience.
@@ -2127,9 +2127,9 @@ mod tests {
terminal.backend().assert_buffer(&expected);
}
/// If there isnt enough room for the selected item and the requested padding the list can jump
/// up and down every frame if something isnt done about it. This code tests to make sure that
/// isnt currently happening
/// If there isn't enough room for the selected item and the requested padding the list can jump
/// up and down every frame if something isn't done about it. This code tests to make sure that
/// isn't currently happening
#[test]
fn test_padding_flicker() {
let backend = backend::TestBackend::new(10, 5);