Make buffers fill from the bottom, and render each paragraph individually
The goal of rendering paragraph individually is to eventually avoid redrawing everything every time there is a change
This commit is contained in:
@ -41,6 +41,7 @@ human-panic = "1.2.0"
|
|||||||
inventory = "0.3"
|
inventory = "0.3"
|
||||||
itertools = "0.11.0"
|
itertools = "0.11.0"
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
lender = "0.2.1"
|
||||||
libc = "0.2.148"
|
libc = "0.2.148"
|
||||||
log = "0.4.20"
|
log = "0.4.20"
|
||||||
nonempty = { version = "0.8.1", features = ["serialize"] }
|
nonempty = { version = "0.8.1", features = ["serialize"] }
|
||||||
@ -61,6 +62,14 @@ crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
|
|||||||
ratatui = { version = "0.24.0", features = ["serde", "macros"] }
|
ratatui = { version = "0.24.0", features = ["serde", "macros"] }
|
||||||
strip-ansi-escapes = "0.2.0"
|
strip-ansi-escapes = "0.2.0"
|
||||||
tui-textarea = "0.3.0"
|
tui-textarea = "0.3.0"
|
||||||
|
unicode-width = "0.1"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
# we need these changes:
|
||||||
|
# * 'make widgets::reflow public' https://github.com/ratatui-org/ratatui/pull/607
|
||||||
|
# * 'define struct WrappedLine instead of anonymous tuple' https://github.com/ratatui-org/ratatui/pull/608
|
||||||
|
ratatui = { git = "https://github.com/progval/ratatui.git", rev = "54a3923b9d5f37da848dbc32a2ffb4eeb4f47490", features = ["serde", "macros"] }
|
||||||
|
#ratatui = { path = "../ratatui", features = ["serde", "macros"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
|
@ -39,20 +39,23 @@ impl Buffer for LogBuffer {
|
|||||||
"ratatrix".to_owned()
|
"ratatrix".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(&self) -> Text {
|
fn content(&self) -> Vec<Text> {
|
||||||
|
use ansi_to_tui::IntoText;
|
||||||
let lines = self
|
let lines = self
|
||||||
.lines
|
.lines
|
||||||
.read()
|
.read()
|
||||||
.expect("LogBuffer could not get log's RwLock as it is poisoned");
|
.expect("LogBuffer could not get log's RwLock as it is poisoned");
|
||||||
let (slice1, slice2) = lines.as_slices();
|
let (slice1, slice2) = lines.as_slices();
|
||||||
let text = if slice1.is_empty() {
|
slice1
|
||||||
slice2.join("\n")
|
.into_iter()
|
||||||
} else if slice2.is_empty() {
|
.chain(slice2.into_iter())
|
||||||
slice1.join("\n")
|
.cloned()
|
||||||
} else {
|
.map(|line| {
|
||||||
format!("{}\n{}", slice1.join("\n"), slice2.join("\n"))
|
line.into_text().unwrap_or_else(|e| {
|
||||||
};
|
tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e);
|
||||||
use ansi_to_tui::IntoText;
|
Text::raw(line)
|
||||||
text.clone().into_text().unwrap_or_else(|_| text.into())
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ pub trait Buffer: Send {
|
|||||||
/// A short human-readable name for the room, eg. to show in compact buflist
|
/// A short human-readable name for the room, eg. to show in compact buflist
|
||||||
fn short_name(&self) -> String;
|
fn short_name(&self) -> String;
|
||||||
async fn poll_updates(&mut self) {}
|
async fn poll_updates(&mut self) {}
|
||||||
fn content(&self) -> ratatui::text::Text;
|
fn content(&self) -> Vec<ratatui::text::Text>; // TODO: make this lazy, only the last few are used
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Buffers {
|
pub struct Buffers {
|
||||||
|
@ -26,6 +26,7 @@ use matrix_sdk::ruma::OwnedRoomId;
|
|||||||
use matrix_sdk::Client;
|
use matrix_sdk::Client;
|
||||||
use matrix_sdk::Room;
|
use matrix_sdk::Room;
|
||||||
use matrix_sdk_ui::timeline::{RoomExt, Timeline, TimelineItem};
|
use matrix_sdk_ui::timeline::{RoomExt, Timeline, TimelineItem};
|
||||||
|
use ratatui::text::Text;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use tokio::pin;
|
use tokio::pin;
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ impl SingleClientRoomBuffer {
|
|||||||
.stream
|
.stream
|
||||||
.next()
|
.next()
|
||||||
.await
|
.await
|
||||||
.map(|change| format!("New item: {:#?}", change)),
|
.map(|change| format!("New items: {:#?}", change)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -123,7 +124,7 @@ impl Buffer for RoomBuffer {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(&self) -> ratatui::text::Text {
|
fn content(&self) -> Vec<Text> {
|
||||||
// TODO: merge buffers, etc.
|
// TODO: merge buffers, etc.
|
||||||
self
|
self
|
||||||
.buffers
|
.buffers
|
||||||
@ -131,7 +132,7 @@ impl Buffer for RoomBuffer {
|
|||||||
.unwrap_or_else(|| panic!("No sub-buffer for {}", self.room_id))
|
.unwrap_or_else(|| panic!("No sub-buffer for {}", self.room_id))
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.join("\n")
|
.map(|line|Text::raw(line))
|
||||||
.into()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,8 @@ use super::Component;
|
|||||||
use color_eyre::eyre::{Result, WrapErr};
|
use color_eyre::eyre::{Result, WrapErr};
|
||||||
use ratatui::{prelude::*, widgets::*};
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
|
||||||
|
use crate::widgets::{BottomAlignedParagraph, OverlappableWidget};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Backlog {}
|
pub struct Backlog {}
|
||||||
|
|
||||||
@ -28,12 +30,18 @@ impl Component for Backlog {
|
|||||||
area: Rect,
|
area: Rect,
|
||||||
buffers: &crate::buffers::Buffers,
|
buffers: &crate::buffers::Buffers,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
frame.render_widget(
|
let block = Block::new().borders(Borders::ALL);
|
||||||
Paragraph::new(buffers.active_buffer().content())
|
let mut text_area = block.inner(area);
|
||||||
.block(Block::new().borders(Borders::ALL))
|
block.render(area, frame.buffer_mut());
|
||||||
.wrap(Wrap { trim: true }),
|
|
||||||
area,
|
let mut items = buffers.active_buffer().content();
|
||||||
);
|
items.reverse();
|
||||||
|
for item in items {
|
||||||
|
let widget = BottomAlignedParagraph::new(item);
|
||||||
|
let height = widget.render_overlap(text_area, frame.buffer_mut());
|
||||||
|
assert!(area.height >= height, "{:?} {}", area, height);
|
||||||
|
text_area.height -= height; // Remove lines at the bottom used by this paragraph
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ pub mod mode;
|
|||||||
pub mod plugins;
|
pub mod plugins;
|
||||||
pub mod tui;
|
pub mod tui;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod widgets;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::Cli;
|
use cli::Cli;
|
||||||
|
107
src/widgets/bottom_aligned_paragraph.rs
Normal file
107
src/widgets/bottom_aligned_paragraph.rs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* 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 ratatui::widgets::reflow::WordWrapper;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
|
use super::OverlappableWidget;
|
||||||
|
|
||||||
|
/// A variant of [`Paragraph`](ratatui::widgets::Paragraph) that implements [`BottomAlignedWidget`]
|
||||||
|
/// and always wraps
|
||||||
|
pub struct BottomAlignedParagraph<'a> {
|
||||||
|
text: Text<'a>,
|
||||||
|
scroll: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 beginning
|
||||||
|
///
|
||||||
|
/// This is like [`Paragraph::scroll`](ratatui::widgets::Paragraph::scroll), but it's only vertical.
|
||||||
|
pub fn scroll(mut self, offset: u16) -> BottomAlignedParagraph<'a> {
|
||||||
|
self.scroll = offset;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 trim = false;
|
||||||
|
let style = Style::default();
|
||||||
|
let lines: Vec<_> = WordWrapper::new(
|
||||||
|
self.text.lines.iter().map(|line| {
|
||||||
|
(
|
||||||
|
line
|
||||||
|
.spans
|
||||||
|
.iter()
|
||||||
|
.flat_map(|span| span.styled_graphemes(style)),
|
||||||
|
Alignment::Left,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
area.width,
|
||||||
|
trim,
|
||||||
|
)
|
||||||
|
.skip(self.scroll as usize)
|
||||||
|
.map_into_iter(|line: ratatui::widgets::reflow::WrappedLine| line.line.to_vec())
|
||||||
|
.collect();
|
||||||
|
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[(lines.len() - text_area_height)..]
|
||||||
|
} else {
|
||||||
|
&lines[..]
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(lines.len() <= text_area_height);
|
||||||
|
|
||||||
|
for (y, line) in lines.into_iter().rev().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
|
||||||
|
}
|
||||||
|
}
|
34
src/widgets/mod.rs
Normal file
34
src/widgets/mod.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* 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::Widget;
|
||||||
|
|
||||||
|
mod bottom_aligned_paragraph;
|
||||||
|
pub use bottom_aligned_paragraph::BottomAlignedParagraph;
|
||||||
|
|
||||||
|
/// A [`Widget`] that returns how many lines it actually drew to.
|
||||||
|
pub trait OverlappableWidget {
|
||||||
|
fn render_overlap(self, area: Rect, buf: &mut Buffer) -> u16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
impl<W: OverlappableWidget> Widget for W {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
self.render_overlap(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
Reference in New Issue
Block a user