Files
ratatrix/src/widgets/bottom_aligned_paragraph.rs
2023-11-03 13:15:38 +01:00

125 lines
3.8 KiB
Rust

/*
* Copyright (C) 2023 Valentin Lorentz
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use lender::{Lender, Lending};
use ratatui::prelude::*;
use ratatui::text::StyledGrapheme;
use unicode_width::UnicodeWidthStr;
use super::OverlappableWidget;
use crate::widgets::reflow::{WordWrapper, WrappedLine};
/// A variant of [`Paragraph`](ratatui::widgets::Paragraph) that implements [`BottomAlignedWidget`]
/// and always wraps
#[derive(Debug)]
pub struct BottomAlignedParagraph<'a> {
text: Text<'a>,
/// Number of lines at the bottom that should not be rendered
scroll: u64,
}
impl<'a> BottomAlignedParagraph<'a> {
pub fn new<T>(text: T) -> BottomAlignedParagraph<'a>
where
T: Into<Text<'a>>,
{
BottomAlignedParagraph {
text: text.into(),
scroll: 0,
}
}
/// How many lines should be skipped at the bottom
///
/// This is like [`Paragraph::scroll`](ratatui::widgets::Paragraph::scroll), but it's only vertical.
pub fn scroll(mut self, offset: u64) -> BottomAlignedParagraph<'a> {
self.scroll = offset;
self
}
/// Returns lines with at most the given width, starting from the last one
fn wrap_lines(&self, width: u16) -> Vec<Vec<StyledGrapheme<'_>>> {
let trim = false;
let style = Style::default();
WordWrapper::new(
self.text.lines.iter().map(|line| {
(
line
.spans
.iter()
.flat_map(|span| span.styled_graphemes(style)),
Alignment::Left,
)
}),
width,
trim,
)
.map_into_iter(|line: crate::widgets::reflow::WrappedLine| line.line.to_vec())
.collect::<Vec<_>>()
.into_iter()
.rev()
.skip(self.scroll as usize)
.take(u16::MAX as usize) // Avoid overflows in ratatui for excessively long messages
.collect()
}
/// Returns how many lines it would actuall draw if rendered with the given height
pub fn height(&self, width: u16) -> u64 {
self.wrap_lines(width).len() as u64
}
}
impl<'a> OverlappableWidget for BottomAlignedParagraph<'a> {
fn render_overlap(self, area: Rect, buf: &mut Buffer) -> u16 {
// Inspired by https://github.com/ratatui-org/ratatui/blob/9f371000968044e09545d66068c4ed4ea4b35d8a/src/widgets/paragraph.rs#L214-L275
let lines = self.wrap_lines(area.width);
let text_area = area; // Borders not supported by BottomAlignedParagraph (yet?)
let text_area_height = text_area.height as usize;
let lines = if lines.len() > text_area_height {
// Overflow; keep only the last lines
&lines[..text_area_height]
} else {
&lines[..]
};
assert!(lines.len() <= text_area_height);
for (y, line) in lines.into_iter().enumerate() {
let mut x = 0;
for StyledGrapheme { symbol, style } in line {
let width = symbol.width();
if width == 0 {
continue;
}
buf
.get_mut(text_area.left() + x, text_area.bottom() - (y as u16) - 1)
.set_symbol(if symbol.is_empty() {
// If the symbol is empty, the last char which rendered last time will
// leave on the line. It's a quick fix.
" "
} else {
symbol
})
.set_style(*style);
x += width as u16;
}
}
lines.len() as u16
}
}