Preserve current position when new items are added to a buffer
All checks were successful
CI / lint (push) Successful in 5m3s
CI / Build and test (, 1.73.0) (push) Successful in 9m34s
CI / Build and test (, beta) (push) Successful in 9m34s
CI / Build and test (, nightly) (push) Successful in 8m46s

This commit is contained in:
2023-11-17 14:14:24 +01:00
parent 9b29d4d9e5
commit dd404147ab
5 changed files with 291 additions and 67 deletions

View File

@ -32,7 +32,7 @@ use crate::widgets::Prerender;
const MAX_MEM_LOG_LINES: usize = 1000;
pub struct LogBuffer {
lines: VecDeque<(String, Prerender)>,
lines: VecDeque<(u64, String, Prerender)>,
receiver: UnboundedReceiver<String>,
}
@ -73,17 +73,29 @@ impl Buffer for LogBuffer {
if self.lines.len() >= MAX_MEM_LOG_LINES {
self.lines.pop_front();
}
self.lines.push_back((line, Prerender::new()));
let line_id = self
.lines
.back()
.map(|(last_id, _, _)| last_id + 1)
.unwrap_or(0);
self.lines.push_back((line_id, line, Prerender::new()));
}
fn content<'a>(&'a self) -> Box<dyn ExactSizeIterator<Item = BufferItem<'a>> + 'a> {
use ansi_to_tui::IntoText;
Box::new(self.lines.iter().rev().map(|(line, prerender)| BufferItem {
content: BufferItemContent::Text(line.clone().into_text().unwrap_or_else(|e| {
tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e);
Text::raw(line)
})),
prerender,
}))
Box::new(
self
.lines
.iter()
.rev()
.map(|(line_id, line, prerender)| BufferItem {
content: BufferItemContent::Text(line.clone().into_text().unwrap_or_else(|e| {
tracing::error!("Could not convert line from ANSI codes to ratatui: {}", e);
Text::raw(line)
})),
prerender,
unique_id: Some(*line_id),
}),
)
}
}

View File

@ -87,9 +87,15 @@ pub enum BufferItemContent<'buf> {
Empty,
}
// intentionally not Clone, because it would be ambiguous what to do
// with the prerender if the content is updated only in one instance
pub struct BufferItem<'buf> {
pub content: BufferItemContent<'buf>,
pub prerender: &'buf Prerender,
/// Used to preserve position when other items are added or edited in recent history.
///
/// Only needs to be unique per-buffer
pub unique_id: Option<u64>,
}
#[async_trait]

View File

@ -90,7 +90,7 @@ pub struct SingleClientRoomBuffer {
room_id: OwnedRoomId,
client: Client,
items: imbl::vector::Vector<(OwnedBufferItemContent, Prerender)>,
items: imbl::vector::Vector<(Option<u64>, OwnedBufferItemContent, Prerender)>,
// TODO: get rid of this trait object, we know it's matrix_sdk_ui::timeline::TimelineStream
timeline_stream: Box<dyn Stream<Item = Vec<VectorDiff<Arc<TimelineItem>>>> + Send + Sync + Unpin>,
timeline: Arc<Timeline>,
@ -105,7 +105,9 @@ impl DynamicUsage for SingleClientRoomBuffer {
+ self
.items
.iter()
.map(|(content, prerender)| content.dynamic_usage() + prerender.dynamic_usage())
.map(|(_, content, prerender)| {
std::mem::size_of::<Option<u64>>() + content.dynamic_usage() + prerender.dynamic_usage()
})
.sum::<usize>()
}
fn dynamic_usage_bounds(&self) -> (usize, Option<usize>) {
@ -117,7 +119,11 @@ impl DynamicUsage for SingleClientRoomBuffer {
.items
.iter()
.map(|item| item.0.dynamic_usage_bounds())
.chain(self.items.iter().map(|item| item.1.dynamic_usage_bounds())),
.chain(self.items.iter().map(|item| item.1.dynamic_usage_bounds()))
.chain([(
std::mem::size_of::<Option<u64>>() * self.items.len(),
Some(std::mem::size_of::<Option<u64>>() * self.items.len()),
)]),
)
}
}
@ -139,7 +145,7 @@ impl SingleClientRoomBuffer {
let Some(changes) = changes else { return None; };
for change in changes {
change
.map(|item| (self.format_timeline_item(item), Prerender::new()))
.map(|item| (Some(item.unique_id()), self.format_timeline_item(item), Prerender::new()))
.apply(&mut self.items);
}
None
@ -400,7 +406,7 @@ impl RoomBuffer {
timeline: Arc::new(timeline),
items: items // FIXME: it's always empty. why?
.into_iter()
.map(|item| (text!("Initial item: {:#?}", item), Prerender::new()))
.map(|item| (None, text!("Initial item: {:#?}", item), Prerender::new()))
.collect(),
timeline_stream: Box::new(timeline_stream),
back_pagination_request: AtomicU16::new(0),
@ -534,13 +540,14 @@ impl Buffer for RoomBuffer {
.items
.iter()
.rev()
.map(|(line, prerender)| BufferItem {
.map(|(line_id, line, prerender)| BufferItem {
content: match line {
OwnedBufferItemContent::Text(text) => BufferItemContent::Text(Text::raw(text)),
OwnedBufferItemContent::Divider(text) => BufferItemContent::Divider(Text::raw(text)),
OwnedBufferItemContent::Empty => BufferItemContent::Empty,
},
prerender,
unique_id: *line_id,
}),
)
}

View File

@ -30,9 +30,24 @@ use crate::widgets::{
use super::Component;
use crate::buffers::{BufferItem, BufferItemContent};
#[derive(Default)]
#[derive(Debug)]
struct ScrollPosition {
/// unique_id of the buffer item the scroll is relative to
anchor: u64,
/// number of lines from the top of the item where the bottom of the viewport is
relative_scroll: i64,
}
#[derive(Default, Debug)]
pub struct Backlog {
scroll: u64,
scroll_position: Option<ScrollPosition>,
/// Fallback used if the scroll_position is missing or unusable
absolute_scroll: u64,
/// How many lines up (or down) the user requested the viewport to move since the last
/// render.
///
/// This is applied to `scroll_position` and `fallback_scroll` on the next render.
pending_scroll: i64,
}
impl Component for Backlog {
@ -67,8 +82,8 @@ impl Component for Backlog {
buffers: &crate::buffers::Buffers,
) -> Result<()> {
let active_buffer = buffers.active_buffer();
let items = active_buffer.content();
let undrawn_widgets_at_top = self.draw_items(frame.buffer_mut(), area, items)?;
let undrawn_widgets_at_top =
self.draw_items(frame.buffer_mut(), area, || active_buffer.content())?;
// We are reaching the end of the backlog we have locally, ask the buffer to fetch
// more if it can
@ -82,10 +97,10 @@ impl Component for Backlog {
impl Backlog {
pub fn scroll_up(&mut self, lines: u64) {
self.scroll = self.scroll.saturating_add(lines);
self.pending_scroll = self.pending_scroll.saturating_add(lines as i64);
}
pub fn scroll_down(&mut self, lines: u64) {
self.scroll = self.scroll.saturating_sub(20);
self.pending_scroll = self.pending_scroll.saturating_sub(lines as i64);
}
fn build_widget<'a>(&self, content: BufferItemContent<'a>, scroll: u64) -> BacklogItemWidget<'a> {
@ -102,50 +117,94 @@ impl Backlog {
}
}
fn get_item_height(&self, item: &BufferItem<'_>, width: u16) -> u64 {
match item.prerender.0.lock().unwrap().deref_mut() {
Some(PrerenderInner {
key,
value: PrerenderValue::Rendered(buf),
}) if *key == width => buf.area().height as u64,
Some(PrerenderInner {
key,
value: PrerenderValue::NotRendered(height),
}) if *key == width => *height,
prerender => {
let widget = self.build_widget(item.content.clone(), 0);
// widget.height() needs to run the whole word-wrapping, which is almost as
// expensive as the real render.
// This is particularly wasteful, as the last widget.height() call here will
// duplicate the work we do in widget.render_overlap() later in the loop.
// Unfortunately I can't find a way to make it work because of the lifetimes
// involved.
let expected_height = widget.height(width);
*prerender = Some(PrerenderInner {
key: width,
value: PrerenderValue::NotRendered(expected_height),
});
expected_height
},
}
}
/// Returns how many items were not drawn because they are too high up the backlog
/// (ie. older than the currently displayed items)
pub fn draw_items<'a>(
pub fn draw_items<'a, Items: ExactSizeIterator<Item = BufferItem<'a>>>(
&mut self,
frame_buffer: &mut Buffer,
area: Rect,
mut items: impl ExactSizeIterator<Item = BufferItem<'a>>,
get_items: impl Fn() -> Items,
) -> Result<usize> {
let block = Block::new().borders(Borders::ALL);
let mut text_area = block.inner(area);
block.render(area, frame_buffer);
let mut scroll = self.scroll;
// Recompute absolute scroll position if we are not at the bottom of the backlog
if self.absolute_scroll != 0 || self.pending_scroll != 0 {
if let Some(scroll_position) = self.scroll_position.as_ref() {
if !get_items().any(|item| item.unique_id == Some(scroll_position.anchor)) {
// The anchor doesn't exist anymore, invalidate it
self.scroll_position = None;
}
}
if let Some(scroll_position) = self.scroll_position.as_ref() {
// Compute the current absolute scroll from the anchor.
let mut found_anchor = false;
self.absolute_scroll = 0;
for item in get_items() {
self.absolute_scroll = self
.absolute_scroll
.saturating_add(self.get_item_height(&item, text_area.width));
if item.unique_id == Some(scroll_position.anchor) {
found_anchor = true;
break;
}
}
assert!(found_anchor, "anchor disappeared between get_items() calls");
self.absolute_scroll = self
.absolute_scroll
.saturating_add_signed(-scroll_position.relative_scroll);
}
if self.pending_scroll != 0 {
self.absolute_scroll = self
.absolute_scroll
.saturating_add_signed(self.pending_scroll);
self.scroll_position = None;
self.pending_scroll = 0;
}
}
assert_eq!(self.pending_scroll, 0, "pending_scroll was not applied");
let mut items = get_items();
let mut scroll = self.absolute_scroll;
// Skip widgets at the bottom (if scrolled up), and render the first visible one
loop {
let Some(item) = items.next() else {
break;
};
let expected_height = match item.prerender.0.lock().unwrap().deref_mut() {
Some(PrerenderInner {
key,
value: PrerenderValue::Rendered(buf),
}) if *key == text_area.width => buf.area().height as u64,
Some(PrerenderInner {
key,
value: PrerenderValue::NotRendered(height),
}) if *key == text_area.width => *height,
prerender => {
let widget = self.build_widget(item.content.clone(), 0);
// widget.height() needs to run the whole word-wrapping, which is almost as
// expensive as the real render.
// This is particularly wasteful, as the last widget.height() call here will
// duplicate the work we do in widget.render_overlap() later in the loop.
// Unfortunately I can't find a way to make it work because of the lifetimes
// involved.
let expected_height = widget.height(text_area.width);
*prerender = Some(PrerenderInner {
key: text_area.width,
value: PrerenderValue::NotRendered(expected_height),
});
expected_height
},
};
let expected_height = self.get_item_height(&item, text_area.width);
if scroll.saturating_sub(expected_height) > text_area.height.into() {
// Paragraph is too far down, not displayed
@ -158,6 +217,18 @@ impl Backlog {
let (_, height) = widget.render_overlap(text_area, frame_buffer);
text_area.height = text_area.height.saturating_sub(height);
if self.absolute_scroll != 0 && expected_height >= scroll {
// If we are not at the bottom of the backlog and this is the first item up enough
// to be visible, set it as anchor for next render
if let Some(anchor) = item.unique_id {
self.scroll_position = Some(ScrollPosition {
anchor,
relative_scroll: (expected_height - scroll) as i64, // legal because always positive
});
}
// TODO: if item.unique_id is None, pick the next item that has an id
}
scroll = scroll.saturating_sub(expected_height);
if scroll == 0 {
break;

View File

@ -31,6 +31,23 @@ fn rect(x: u16, y: u16, width: u16, height: u16) -> Rect {
}
}
macro_rules! items_iter {
[ $($item: expr),* ] => {
|| vec![
$(
{
let BufferItem { content, prerender, unique_id } = &$item;
BufferItem {
content: content.clone(),
prerender,
unique_id: *unique_id,
}
},
)*
].into_iter()
}
}
#[test]
fn test_single_item() {
let mut bl = Backlog::default();
@ -38,9 +55,10 @@ fn test_single_item() {
let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")),
prerender: &prerender,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 18, 8));
bl.draw_items(&mut buf, rect(3, 2, 12, 4), vec![item].into_iter())
bl.draw_items(&mut buf, rect(3, 2, 12, 4), items_iter![item])
.context("Failed to draw")
.unwrap();
@ -64,10 +82,11 @@ fn test_single_item_cached() {
let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")),
prerender: &prerender,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 18, 8));
let area = rect(3, 2, 12, 4);
bl.draw_items(&mut buf, area, vec![item].into_iter())
bl.draw_items(&mut buf, area, items_iter![item])
.context("Failed to draw")
.unwrap();
@ -88,9 +107,10 @@ fn test_single_item_cached() {
let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")),
prerender: &prerender,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 18, 8));
bl.draw_items(&mut buf, area, vec![item].into_iter())
bl.draw_items(&mut buf, area, items_iter![item])
.context("Failed to draw")
.unwrap();
assert_eq!(buf, expected);
@ -105,16 +125,18 @@ fn test_only_necessary_width() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")),
prerender: &prerender1,
unique_id: None,
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw(":)")),
prerender: &prerender2,
unique_id: None,
};
let mut cell = ratatui::buffer::Cell::default();
cell.set_char('.');
let mut buf = Buffer::filled(rect(0, 0, 18, 7), &cell); // poisoned buffer
let area = rect(3, 1, 12, 5);
bl.draw_items(&mut buf, area, vec![item2, item1].into_iter())
bl.draw_items(&mut buf, area, items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
@ -134,13 +156,15 @@ fn test_only_necessary_width() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")),
prerender: &prerender1,
unique_id: None,
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw(":)")),
prerender: &prerender2,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 18, 7));
bl.draw_items(&mut buf, area, vec![item2, item1].into_iter())
bl.draw_items(&mut buf, area, items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
let expected = Buffer::with_lines(vec![
@ -162,9 +186,10 @@ fn test_single_item_tight() {
let item = BufferItem {
content: BufferItemContent::Text(Text::raw("hello")),
prerender: &prerender,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 13, 7));
bl.draw_items(&mut buf, rect(3, 2, 7, 3), vec![item].into_iter())
bl.draw_items(&mut buf, rect(3, 2, 7, 3), items_iter![item])
.context("Failed to draw")
.unwrap();
@ -187,14 +212,16 @@ fn test_two_items() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: None,
};
let prerender2 = Prerender::new();
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")),
prerender: &prerender2,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), vec![item2, item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
@ -219,13 +246,15 @@ fn test_two_items_scroll() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: Some(123),
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")),
prerender: &prerender2,
unique_id: Some(456),
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), vec![item2, item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
@ -245,13 +274,15 @@ fn test_two_items_scroll() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: Some(123),
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")),
prerender: &prerender2,
unique_id: Some(456),
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), vec![item2, item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
@ -271,13 +302,15 @@ fn test_two_items_scroll() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: Some(123),
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")),
prerender: &prerender2,
unique_id: Some(456),
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), vec![item2, item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
@ -300,14 +333,16 @@ fn test_two_items_multiline() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: None,
};
let prerender2 = Prerender::new();
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world\n!")),
prerender: &prerender2,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), vec![item2, item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
@ -330,14 +365,16 @@ fn test_two_items_tight() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: None,
};
let prerender2 = Prerender::new();
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")),
prerender: &prerender2,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 9, 6));
bl.draw_items(&mut buf, rect(1, 1, 7, 4), vec![item2, item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 7, 4), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
@ -359,11 +396,12 @@ fn test_cache_moved() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: None,
};
// Draw once
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), vec![item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item1])
.context("Failed to draw")
.unwrap();
let expected = Buffer::with_lines(vec![
@ -381,14 +419,16 @@ fn test_cache_moved() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi")),
prerender: &prerender1,
unique_id: None,
};
let prerender2 = Prerender::new();
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("world")),
prerender: &prerender2,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), vec![item2, item1].into_iter())
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
let expected = Buffer::with_lines(vec![
@ -413,20 +453,23 @@ fn test_overflow_and_scroll() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")),
prerender: &prerender1,
unique_id: None,
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2,
unique_id: None,
};
let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")),
prerender: &prerender3,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(
&mut buf,
rect(1, 1, 12, 5),
vec![item3, item2, item1].into_iter(),
items_iter![item3, item2, item1],
)
.context("Failed to draw")
.unwrap();
@ -446,20 +489,23 @@ fn test_overflow_and_scroll() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")),
prerender: &prerender1,
unique_id: None,
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2,
unique_id: None,
};
let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")),
prerender: &prerender3,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(
&mut buf,
rect(1, 1, 12, 5),
vec![item3, item2, item1].into_iter(),
items_iter![item3, item2, item1],
)
.context("Failed to draw")
.unwrap();
@ -479,20 +525,23 @@ fn test_overflow_and_scroll() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")),
prerender: &prerender1,
unique_id: None,
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2,
unique_id: None,
};
let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")),
prerender: &prerender3,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(
&mut buf,
rect(1, 1, 12, 5),
vec![item3, item2, item1].into_iter(),
items_iter![item3, item2, item1],
)
.context("Failed to draw")
.unwrap();
@ -511,20 +560,23 @@ fn test_overflow_and_scroll() {
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("line1 x")),
prerender: &prerender1,
unique_id: None,
};
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("line2 y\nline3 y\nline4 y")),
prerender: &prerender2,
unique_id: None,
};
let item3 = BufferItem {
content: BufferItemContent::Text(Text::raw("line5 z")),
prerender: &prerender3,
unique_id: None,
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(
&mut buf,
rect(1, 1, 12, 5),
vec![item3, item2, item1].into_iter(),
items_iter![item3, item2, item1],
)
.context("Failed to draw")
.unwrap();
@ -539,3 +591,79 @@ fn test_overflow_and_scroll() {
]);
assert_eq!(buf, expected);
}
#[test]
fn test_scrolledup_new_line() {
let mut bl = Backlog::default();
let prerender1 = Prerender::new();
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")),
prerender: &prerender1,
unique_id: Some(123),
};
// Draw once
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item1])
.context("Failed to draw")
.unwrap();
let expected = Buffer::with_lines(vec![
" ",
" ┌──────────┐ ",
" │ │ ",
" │hi │ ",
" │world │ ",
" └──────────┘ ",
" ",
]);
assert_eq!(buf, expected);
// Scroll up one line
bl.scroll_up(1);
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")),
prerender: &prerender1,
unique_id: Some(123),
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item1])
.context("Failed to draw")
.unwrap();
let expected = Buffer::with_lines(vec![
" ",
" ┌──────────┐ ",
" │ │ ",
" │ │ ",
" │hi │ ",
" └──────────┘ ",
" ",
]);
assert_eq!(buf, expected);
// New item added at bottom, displayed paragraph should not move up
let item1 = BufferItem {
content: BufferItemContent::Text(Text::raw("hi\nworld")),
prerender: &prerender1,
unique_id: Some(123),
};
let prerender2 = Prerender::new();
let item2 = BufferItem {
content: BufferItemContent::Text(Text::raw("!")),
prerender: &prerender2,
unique_id: Some(456),
};
let mut buf = Buffer::empty(rect(0, 0, 14, 7));
bl.draw_items(&mut buf, rect(1, 1, 12, 5), items_iter![item2, item1])
.context("Failed to draw")
.unwrap();
let expected = Buffer::with_lines(vec![
" ",
" ┌──────────┐ ",
" │ │ ",
" │ │ ",
" │hi │ ",
" └──────────┘ ",
" ",
]);
assert_eq!(buf, expected);
}