Compare commits
5 Commits
v0.27.0-al
...
v0.27.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8719608bda | ||
|
|
f6c4e447e6 | ||
|
|
c56f49b9fb | ||
|
|
078e97e4ff | ||
|
|
8e68db9e2f |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -5,4 +5,4 @@
|
||||
# https://git-scm.com/docs/gitignore#_pattern_format
|
||||
|
||||
# Maintainers
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271
|
||||
* @orhun @mindoodoo @sayanarijit @joshka @kdheepak @Valentin271 @EdJoPaTo
|
||||
|
||||
@@ -282,7 +282,7 @@ impl App {
|
||||
|
||||
fn header() -> impl Widget {
|
||||
let text = "Constraint Explorer";
|
||||
text.bold().fg(Self::HEADER_COLOR).to_centered_line()
|
||||
text.bold().fg(Self::HEADER_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
fn instructions() -> impl Widget {
|
||||
@@ -541,7 +541,7 @@ impl SpacerBlock {
|
||||
/// A label that says "Spacer" if there is enough space
|
||||
fn spacer_label(width: u16) -> impl Widget {
|
||||
let label = if width >= 6 { "Spacer" } else { "" };
|
||||
label.fg(Self::TEXT_COLOR).to_centered_line()
|
||||
label.fg(Self::TEXT_COLOR).into_centered_line()
|
||||
}
|
||||
|
||||
/// A label that says "8 px" if there is enough space
|
||||
|
||||
@@ -304,7 +304,6 @@ impl From<CColor> for Color {
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
|
||||
@@ -209,16 +209,13 @@ where
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Fg(Color);
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct Bg(Color);
|
||||
|
||||
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
|
||||
/// values. This is useful when updating the terminal display, as it allows for more
|
||||
/// efficient updates by only sending the necessary changes.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
struct ModifierDiff {
|
||||
from: Modifier,
|
||||
to: Modifier,
|
||||
|
||||
@@ -282,36 +282,56 @@ impl<'a> Span<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_left_aligned_line();
|
||||
/// let line = "Test Content".green().italic().into_left_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn to_left_aligned_line(self) -> Line<'a> {
|
||||
pub fn into_left_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).left_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_left_aligned_line"]
|
||||
pub fn to_left_aligned_line(self) -> Line<'a> {
|
||||
self.into_left_aligned_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a center-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_centered_line();
|
||||
/// let line = "Test Content".green().italic().into_centered_line();
|
||||
/// ```
|
||||
pub fn to_centered_line(self) -> Line<'a> {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_centered_line(self) -> Line<'a> {
|
||||
Line::from(self).centered()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_centered_line"]
|
||||
pub fn to_centered_line(self) -> Line<'a> {
|
||||
self.into_centered_line()
|
||||
}
|
||||
|
||||
/// Converts this Span into a right-aligned [`Line`]
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// let l = "Test Content".green().italic().to_right_aligned_line();
|
||||
/// let line = "Test Content".green().italic().into_right_aligned_line();
|
||||
/// ```
|
||||
pub fn to_right_aligned_line(self) -> Line<'a> {
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn into_right_aligned_line(self) -> Line<'a> {
|
||||
Line::from(self).right_aligned()
|
||||
}
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
#[deprecated = "use into_right_aligned_line"]
|
||||
pub fn to_right_aligned_line(self) -> Line<'a> {
|
||||
self.into_right_aligned_line()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for Span<'a>
|
||||
@@ -616,21 +636,21 @@ mod tests {
|
||||
#[test]
|
||||
fn left_aligned() {
|
||||
let span = Span::styled("Test Content", Style::new().green().italic());
|
||||
let line = span.to_left_aligned_line();
|
||||
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.to_centered_line();
|
||||
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.to_right_aligned_line();
|
||||
let line = span.into_right_aligned_line();
|
||||
assert_eq!(line.alignment, Some(Alignment::Right));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ pub struct Label<'a> {
|
||||
///
|
||||
/// This allows the canvas to be drawn in multiple layers. This is useful if you want to draw
|
||||
/// multiple shapes on the canvas in specific order.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug)]
|
||||
struct Layer {
|
||||
// A string of characters representing the grid. This will be wrapped to the width of the grid
|
||||
// when rendering
|
||||
@@ -97,7 +97,7 @@ trait Grid: Debug {
|
||||
///
|
||||
/// This grid type only supports a single foreground color for each 2x4 dots cell. There is no way
|
||||
/// to set the individual color of each dot in the braille pattern.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug)]
|
||||
struct BrailleGrid {
|
||||
/// Width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
@@ -160,7 +160,7 @@ impl Grid for BrailleGrid {
|
||||
///
|
||||
/// This makes it possible to draw shapes with a resolution of 1x1 dots per cell. This is useful
|
||||
/// when you want to draw shapes with a low resolution.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug)]
|
||||
struct CharGrid {
|
||||
/// Width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
@@ -232,7 +232,7 @@ impl Grid for CharGrid {
|
||||
/// This allows for more flexibility than the `BrailleGrid` which only supports a single
|
||||
/// foreground color for each 2x4 dots cell, and the `CharGrid` which only supports a single
|
||||
/// character for each cell.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug)]
|
||||
struct HalfBlockGrid {
|
||||
/// Width of the grid in number of terminal columns
|
||||
width: u16,
|
||||
|
||||
@@ -400,7 +400,6 @@ impl<'a> Dataset<'a> {
|
||||
|
||||
/// A container that holds all the infos about where to display each elements of the chart (axis,
|
||||
/// labels, legend, ...).
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
struct ChartLayout {
|
||||
/// Location of the title of the x axis
|
||||
title_x: Option<(u16, u16)>,
|
||||
@@ -691,50 +690,63 @@ impl<'a> Chart<'a> {
|
||||
|
||||
/// Compute the internal layout of the chart given the area. If the area is too small some
|
||||
/// elements may be automatically hidden
|
||||
fn layout(&self, area: Rect) -> ChartLayout {
|
||||
let mut layout = ChartLayout::default();
|
||||
fn layout(&self, area: Rect) -> Option<ChartLayout> {
|
||||
if area.height == 0 || area.width == 0 {
|
||||
return layout;
|
||||
return None;
|
||||
}
|
||||
let mut x = area.left();
|
||||
let mut y = area.bottom() - 1;
|
||||
|
||||
let mut label_x = None;
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.label_x = Some(y);
|
||||
label_x = Some(y);
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
layout.label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||
let label_y = self.y_axis.labels.as_ref().and(Some(x));
|
||||
x += self.max_width_of_labels_left_of_y_axis(area, self.y_axis.labels.is_some());
|
||||
|
||||
let mut axis_x = None;
|
||||
if self.x_axis.labels.is_some() && y > area.top() {
|
||||
layout.axis_x = Some(y);
|
||||
axis_x = Some(y);
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
let mut axis_y = None;
|
||||
if self.y_axis.labels.is_some() && x + 1 < area.right() {
|
||||
layout.axis_y = Some(x);
|
||||
axis_y = Some(x);
|
||||
x += 1;
|
||||
}
|
||||
|
||||
if x < area.right() && y > 1 {
|
||||
layout.graph_area = Rect::new(x, area.top(), area.right() - x, y - area.top() + 1);
|
||||
}
|
||||
let graph_width = area.right().saturating_sub(x);
|
||||
let graph_height = y.saturating_sub(area.top()).saturating_add(1);
|
||||
debug_assert_ne!(
|
||||
graph_width, 0,
|
||||
"Axis and labels should have been hidden due to the small area"
|
||||
);
|
||||
debug_assert_ne!(
|
||||
graph_height, 0,
|
||||
"Axis and labels should have been hidden due to the small area"
|
||||
);
|
||||
let graph_area = Rect::new(x, area.top(), graph_width, graph_height);
|
||||
|
||||
let mut title_x = None;
|
||||
if let Some(ref title) = self.x_axis.title {
|
||||
let w = title.width() as u16;
|
||||
if w < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||
layout.title_x = Some((x + layout.graph_area.width - w, y));
|
||||
if w < graph_area.width && graph_area.height > 2 {
|
||||
title_x = Some((x + graph_area.width - w, y));
|
||||
}
|
||||
}
|
||||
|
||||
let mut title_y = None;
|
||||
if let Some(ref title) = self.y_axis.title {
|
||||
let w = title.width() as u16;
|
||||
if w + 1 < layout.graph_area.width && layout.graph_area.height > 2 {
|
||||
layout.title_y = Some((x, area.top()));
|
||||
if w + 1 < graph_area.width && graph_area.height > 2 {
|
||||
title_y = Some((x, area.top()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut legend_area = None;
|
||||
if let Some(legend_position) = self.legend_position {
|
||||
let legends = self
|
||||
.datasets
|
||||
@@ -747,27 +759,25 @@ impl<'a> Chart<'a> {
|
||||
|
||||
let [max_legend_width] = Layout::horizontal([self.hidden_legend_constraints.0])
|
||||
.flex(Flex::Start)
|
||||
.areas(layout.graph_area);
|
||||
.areas(graph_area);
|
||||
|
||||
let [max_legend_height] = Layout::vertical([self.hidden_legend_constraints.1])
|
||||
.flex(Flex::Start)
|
||||
.areas(layout.graph_area);
|
||||
.areas(graph_area);
|
||||
|
||||
if inner_width > 0
|
||||
&& legend_width <= max_legend_width.width
|
||||
&& legend_height <= max_legend_height.height
|
||||
{
|
||||
layout.legend_area = legend_position.layout(
|
||||
layout.graph_area,
|
||||
legend_area = legend_position.layout(
|
||||
graph_area,
|
||||
legend_width,
|
||||
legend_height,
|
||||
layout
|
||||
.title_x
|
||||
title_x
|
||||
.and(self.x_axis.title.as_ref())
|
||||
.map(|t| t.width() as u16)
|
||||
.unwrap_or_default(),
|
||||
layout
|
||||
.title_y
|
||||
title_y
|
||||
.and(self.y_axis.title.as_ref())
|
||||
.map(|t| t.width() as u16)
|
||||
.unwrap_or_default(),
|
||||
@@ -775,7 +785,16 @@ impl<'a> Chart<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
layout
|
||||
Some(ChartLayout {
|
||||
title_x,
|
||||
title_y,
|
||||
label_x,
|
||||
label_y,
|
||||
axis_x,
|
||||
axis_y,
|
||||
legend_area,
|
||||
graph_area,
|
||||
})
|
||||
}
|
||||
|
||||
fn max_width_of_labels_left_of_y_axis(&self, area: Rect, has_y_axis: bool) -> u16 {
|
||||
@@ -930,21 +949,16 @@ impl WidgetRef for Chart<'_> {
|
||||
|
||||
self.block.render_ref(area, buf);
|
||||
let chart_area = self.block.inner_if_some(area);
|
||||
if chart_area.is_empty() {
|
||||
let Some(layout) = self.layout(chart_area) else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let graph_area = layout.graph_area;
|
||||
|
||||
// Sample the style of the entire widget. This sample will be used to reset the style of
|
||||
// the cells that are part of the components put on top of the grah area (i.e legend and
|
||||
// axis names).
|
||||
let original_style = buf.get(area.left(), area.top()).style();
|
||||
|
||||
let layout = self.layout(chart_area);
|
||||
let graph_area = layout.graph_area;
|
||||
if graph_area.width < 1 || graph_area.height < 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.render_x_labels(buf, &layout, chart_area, graph_area);
|
||||
self.render_y_labels(buf, &layout, chart_area, graph_area);
|
||||
|
||||
@@ -1136,7 +1150,7 @@ mod tests {
|
||||
.x_axis(Axis::default().title("X axis"))
|
||||
.y_axis(Axis::default().title("Y axis"))
|
||||
.hidden_legend_constraints(case.hidden_legend_constraints);
|
||||
let layout = chart.layout(case.chart_area);
|
||||
let layout = chart.layout(case.chart_area).unwrap();
|
||||
assert_eq!(layout.legend_area, case.legend_area);
|
||||
}
|
||||
}
|
||||
@@ -1208,7 +1222,7 @@ mod tests {
|
||||
let data_unnamed = Dataset::default(); // must not occupy a row in legend
|
||||
let widget = Chart::new(vec![data_named_1, data_unnamed, data_named_2]);
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
|
||||
let layout = widget.layout(buffer.area);
|
||||
let layout = widget.layout(buffer.area).unwrap();
|
||||
|
||||
assert!(layout.legend_area.is_some());
|
||||
assert_eq!(layout.legend_area.unwrap().height, 4); // 2 for borders, 2 for rows
|
||||
@@ -1219,7 +1233,7 @@ mod tests {
|
||||
let dataset = Dataset::default();
|
||||
let widget = Chart::new(vec![dataset; 3]);
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 50, 25));
|
||||
let layout = widget.layout(buffer.area);
|
||||
let layout = widget.layout(buffer.area).unwrap();
|
||||
|
||||
assert!(layout.legend_area.is_none());
|
||||
}
|
||||
|
||||
@@ -973,7 +973,7 @@ impl StatefulWidgetRef for List<'_> {
|
||||
let highlight_symbol_width = self.highlight_symbol.unwrap_or("").width() as u16;
|
||||
Rect {
|
||||
x: row_area.x + highlight_symbol_width,
|
||||
width: row_area.width - highlight_symbol_width,
|
||||
width: row_area.width.saturating_sub(highlight_symbol_width),
|
||||
..row_area
|
||||
}
|
||||
} else {
|
||||
@@ -1046,7 +1046,7 @@ mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{assert_buffer_eq, widgets::Borders};
|
||||
@@ -2234,4 +2234,30 @@ mod tests {
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn single_line_buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 10, 1))
|
||||
}
|
||||
|
||||
/// Regression test for a bug where highlight symbol being greater than width caused a panic due
|
||||
/// to subtraction with underflow.
|
||||
///
|
||||
/// See [#949](https://github.com/ratatui-org/ratatui/pull/949) for details
|
||||
#[rstest]
|
||||
#[case::under(">>>>", "Item1", ">>>>Item1 ")] // enough space to render the highlight symbol
|
||||
#[case::exact(">>>>>", "Item1", ">>>>>Item1")] // exact space to render the highlight symbol
|
||||
#[case::overflow(">>>>>>", "Item1", ">>>>>>Item")] // not enough space
|
||||
fn highlight_symbol_overflow(
|
||||
#[case] highlight_symbol: &str,
|
||||
#[case] item: &str,
|
||||
#[case] expected: &str,
|
||||
mut single_line_buf: Buffer,
|
||||
) {
|
||||
let list = List::new(vec![item]).highlight_symbol(highlight_symbol);
|
||||
let mut state = ListState::default();
|
||||
state.select(Some(0));
|
||||
StatefulWidget::render(list, single_line_buf.area, &mut single_line_buf, &mut state);
|
||||
assert_buffer_eq!(single_line_buf, Buffer::with_lines(vec![expected]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,8 +338,9 @@ impl Paragraph<'_> {
|
||||
return;
|
||||
}
|
||||
|
||||
buf.set_style(text_area, self.style);
|
||||
let styled = self.text.iter().map(|line| {
|
||||
let graphemes = line.styled_graphemes(self.style);
|
||||
let graphemes = line.styled_graphemes(self.text.style);
|
||||
let alignment = line.alignment.unwrap_or(self.alignment);
|
||||
(graphemes, alignment)
|
||||
});
|
||||
@@ -1011,4 +1012,26 @@ mod test {
|
||||
let p = Paragraph::new("Hello, world!").right_aligned();
|
||||
assert_eq!(p.alignment, Alignment::Right);
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/ratatui-org/ratatui/issues/990>
|
||||
///
|
||||
/// This test ensures that paragraphs with a block and styled text are rendered correctly.
|
||||
/// It has been simplified from the original issue but tests the same functionality.
|
||||
#[test]
|
||||
fn paragraph_block_text_style() {
|
||||
let text = Text::styled("Styled text", Color::Green);
|
||||
let paragraph = Paragraph::new(text).block(Block::bordered());
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 20, 3));
|
||||
paragraph.render(Rect::new(0, 0, 20, 3), &mut buf);
|
||||
|
||||
let mut expected = Buffer::with_lines(vec![
|
||||
"┌──────────────────┐",
|
||||
"│Styled text │",
|
||||
"└──────────────────┘",
|
||||
]);
|
||||
expected.set_style(Rect::new(1, 1, 11, 1), Style::default().fg(Color::Green));
|
||||
|
||||
assert_eq!(buf, expected);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user