#[cfg(feature = "egc")]
use alloc::vec::Vec;
use retroglyph_core::grid::Rect;
#[cfg(feature = "egc")]
use retroglyph_core::layout::wrap;
use retroglyph_core::text::Line;
use super::{Measure, PrintLine, Widget};
use crate::Surface;
#[derive(Clone, Copy, Debug)]
pub struct Log<'a> {
messages: &'a [Line],
offset: usize,
#[cfg(feature = "egc")]
wrap: bool,
}
impl<'a> Log<'a> {
#[must_use]
pub const fn new(messages: &'a [Line]) -> Self {
Self {
messages,
offset: 0,
#[cfg(feature = "egc")]
wrap: false,
}
}
#[must_use]
pub const fn offset(mut self, offset: usize) -> Self {
self.offset = offset;
self
}
#[cfg(feature = "egc")]
#[must_use]
pub const fn wrap(mut self, wrap: bool) -> Self {
self.wrap = wrap;
self
}
}
impl Measure for Log<'_> {
fn height_for(&self, width: u16) -> u16 {
#[cfg(feature = "egc")]
if self.wrap {
let total: usize = self.messages.iter().map(|m| wrap(m, width).len()).sum();
#[allow(clippy::cast_possible_truncation)]
let height = total.min(usize::from(u16::MAX)) as u16;
return height;
}
#[cfg(not(feature = "egc"))]
let _ = width;
#[allow(clippy::cast_possible_truncation)]
let height = self.messages.len().min(usize::from(u16::MAX)) as u16;
height
}
}
impl Widget for Log<'_> {
fn render(&self, surface: &mut Surface<'_>) {
let (width, height) = (surface.width(), surface.height());
let visible_height = usize::from(height);
if width == 0 || visible_height == 0 {
return;
}
let Some(bottom) = self
.messages
.len()
.checked_sub(self.offset.saturating_add(1))
else {
return;
};
#[cfg(feature = "egc")]
if self.wrap {
self.render_wrapped(surface, bottom, visible_height, width);
return;
}
let top = bottom.saturating_sub(visible_height - 1);
let area = surface.area();
for (row, message) in self.messages[top..=bottom].iter().enumerate() {
#[allow(clippy::cast_possible_truncation)]
let y = area.top() + row as u16;
let row_area = Rect::new(area.left(), y, width, 1);
PrintLine::new(message).render(&mut surface.scope(row_area));
}
}
}
#[cfg(feature = "egc")]
impl Log<'_> {
fn render_wrapped(
&self,
surface: &mut Surface<'_>,
bottom: usize,
visible_height: usize,
width: u16,
) {
let mut included: Vec<Vec<Line>> = Vec::new();
let mut used = 0usize;
for message in self.messages[..=bottom].iter().rev() {
let rows = wrap(message, width);
if used + rows.len() > visible_height {
break;
}
used += rows.len();
included.push(rows);
}
let area = surface.area();
let mut y = area.top();
for rows in included.into_iter().rev() {
for row in rows {
let row_area = Rect::new(area.left(), y, width, 1);
PrintLine::new(&row).render(&mut surface.scope(row_area));
y += 1;
}
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use retroglyph_core::grid::{Grid, Pos};
use super::*;
fn lines(texts: &[&str]) -> Vec<Line> {
texts.iter().map(|t| Line::raw(*t)).collect()
}
#[test]
fn shows_the_most_recent_messages_oldest_at_top() {
let area = Rect::new(0, 0, 20, 2);
let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
let mut grid = Grid::new(20, 2);
Log::new(&messages).render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'c'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'd'); }
#[test]
fn height_for_is_the_message_count() {
let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
assert_eq!(Log::new(&messages).height_for(80), 4);
}
#[test]
fn offset_scrolls_back_through_history() {
let area = Rect::new(0, 0, 20, 2);
let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
let mut grid = Grid::new(20, 2);
Log::new(&messages)
.offset(1)
.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'b'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'c'); }
#[test]
fn offset_past_the_start_shows_fewer_lines_without_panicking() {
let area = Rect::new(0, 0, 20, 2);
let messages = lines(&["alpha", "bravo"]);
let mut grid = Grid::new(20, 2);
Log::new(&messages)
.offset(5)
.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
}
#[test]
fn fewer_messages_than_visible_rows_leaves_the_rest_untouched() {
let area = Rect::new(0, 0, 20, 4);
let messages = lines(&["only"]);
let mut grid = Grid::new(20, 4);
Log::new(&messages).render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'o'); assert_eq!(grid[Pos::new(0, 1)].glyph(), ' '); assert_eq!(grid[Pos::new(0, 2)].glyph(), ' '); }
#[test]
fn clips_long_lines_to_area_width() {
let area = Rect::new(0, 0, 5, 1);
let messages = lines(&["a much longer message than fits"]);
let mut grid = Grid::new(5, 1);
Log::new(&messages).render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(4, 0)].glyph(), 'c');
}
#[cfg(feature = "egc")]
#[test]
fn wrap_word_wraps_a_long_message_across_rows() {
let area = Rect::new(0, 0, 7, 3);
let messages = lines(&["hello world"]);
let mut grid = Grid::new(7, 3);
Log::new(&messages)
.wrap(true)
.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'w'); assert_eq!(grid[Pos::new(0, 2)].glyph(), ' '); }
#[cfg(feature = "egc")]
#[test]
fn height_for_sums_wrapped_rows_when_wrap_is_set() {
let messages = lines(&["hello world", "hi"]);
assert_eq!(Log::new(&messages).wrap(true).height_for(7), 3); assert_eq!(Log::new(&messages).height_for(7), 2); }
#[cfg(feature = "egc")]
#[test]
fn wrap_keeps_offset_counting_whole_messages_not_rows() {
let area = Rect::new(0, 0, 7, 4);
let messages = lines(&["hello world", "hi"]);
let mut grid = Grid::new(7, 4);
Log::new(&messages)
.wrap(true)
.offset(1)
.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'w'); assert_eq!(grid[Pos::new(0, 2)].glyph(), ' '); }
#[cfg(feature = "egc")]
#[test]
fn wrap_leaves_out_a_message_that_would_only_partially_fit() {
let area = Rect::new(0, 0, 7, 2);
let messages = lines(&["hello world", "hi"]);
let mut grid = Grid::new(7, 2);
Log::new(&messages)
.wrap(true)
.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h'); assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
assert_eq!(grid[Pos::new(0, 1)].glyph(), ' '); }
}