125 lines
3.8 KiB
Rust
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
|
|
}
|
|
}
|