ratatrix/src/widgets/bottom_aligned_container.rs

134 lines
4.1 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 ratatui::prelude::*;
use ratatui::widgets::{Paragraph, Widget};
use super::{BottomAlignedParagraph, OverlappableWidget};
/// Container of multiple [`BottomAlignedParagraph`]
#[derive(Debug)]
pub struct BottomAlignedContainer<'a> {
/// Pairs of `(padding, content)`
paragraphs: Vec<(String, BottomAlignedParagraph<'a>)>,
/// Number of lines at the bottom that should not be rendered
scroll: u64,
}
impl<'a> BottomAlignedContainer<'a> {
pub fn new(paragraphs: Vec<(String, BottomAlignedParagraph<'a>)>) -> BottomAlignedContainer<'a> {
BottomAlignedContainer {
paragraphs,
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) -> BottomAlignedContainer<'a> {
self.scroll = offset;
self
}
fn padding_width(padding: &Text<'_>, max_width: u16) -> u16 {
usize::min(
(max_width - 10).into(), // TODO: make the minimum content width (10) configurable
padding.width(),
) as u16
}
}
impl<'a> OverlappableWidget for BottomAlignedContainer<'a> {
fn height(&self, width: u16) -> u64 {
self
.paragraphs
.iter()
.map(|(padding, paragraph)| {
paragraph.height(width - Self::padding_width(&Line::raw(padding).into(), width))
})
.sum()
}
fn render_overlap(self, mut area: Rect, buf: &mut Buffer) -> (u16, u16) {
if area.height == 0 {
// Don't even bother
return (0, 0);
}
let mut scroll = self.scroll;
let mut actual_width = 0u16;
let mut actual_height = 0usize;
for (padding, paragraph) in self.paragraphs.into_iter().rev() {
let padding: Text<'_> = Line::raw(&padding).into();
let padding_width = Self::padding_width(&padding, area.width);
assert_eq!(
padding.height(),
1,
"Unexpected padding height: {} (padding={:?})",
padding.height(),
padding
);
// FIXME: paragraph.height() is expensive because it needs to run line-wrapping,
// and paragraph.render_overlap then needs to run it again twice.
let paragraph_height = paragraph.height(area.width - padding_width);
if paragraph_height <= scroll {
// Paragraph is under the viewport, don't render it
scroll -= paragraph_height;
} else {
let paragraph = paragraph.scroll(scroll);
let (actual_paragraph_width, actual_paragraph_height) = paragraph.render_overlap(
Rect {
x: area.x + padding_width,
width: area.width - padding_width,
..area
},
buf,
);
scroll = 0;
// Write the padding on each line the paragraph was rendered on
for y in (u16::max(
area.top(),
area.bottom().saturating_sub(actual_paragraph_height),
))..area.bottom()
{
Paragraph::new(padding.clone()).render(
Rect {
x: area.x,
y,
height: 1,
width: padding_width,
},
buf,
);
}
area.height = area.height.saturating_sub(actual_paragraph_height);
actual_height = actual_height.saturating_add(actual_paragraph_height.into());
actual_width = u16::max(actual_width, padding_width + actual_paragraph_width);
if area.height == 0 {
break;
}
}
}
(actual_width, actual_height as u16)
}
}