use retroglyph_core::{Color, Rect, Style};
use super::StatefulWidget;
use super::window::visible_window;
use crate::ListState;
use crate::Surface;
use crate::Theme;
use crate::draw::fill_rect;
use crate::text::truncate as truncate_to_cols;
#[derive(Clone, Copy, Debug)]
pub struct List<'a> {
items: &'a [&'a str],
item_style: Style,
selected_style: Style,
}
impl<'a> List<'a> {
#[must_use]
pub fn new(items: &'a [&'a str]) -> Self {
Self {
items,
item_style: Style::new().fg(Color::Rgb {
r: 170,
g: 175,
b: 190,
}),
selected_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
r: 40,
g: 60,
b: 90,
}),
}
}
#[must_use]
pub const fn item_style(mut self, style: Style) -> Self {
self.item_style = style;
self
}
#[must_use]
pub const fn selected_style(mut self, style: Style) -> Self {
self.selected_style = style;
self
}
#[must_use]
pub fn theme(self, theme: Theme) -> Self {
self.theme_on(theme, theme.panel_bg)
}
#[must_use]
pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
self.item_style = Style::new().fg(theme.fg).bg(bg);
self.selected_style = Style::new().fg(theme.bg).bg(theme.accent);
self
}
}
impl StatefulWidget for List<'_> {
type State = ListState;
fn render(&self, area: Rect, surface: &mut Surface<'_>, state: &mut Self::State) {
if area.width() == 0 || area.height() == 0 {
return;
}
let visible_items = area.height_usize();
let selected = state.selected();
for (item_index, &item) in visible_window(self.items, state.offset(), visible_items) {
#[allow(clippy::cast_possible_truncation)]
let row = (item_index - state.offset()) as u16;
let y = area.top() + row;
let style = if Some(item_index) == selected {
fill_rect(
surface,
Rect::new(area.left(), y, area.width(), 1),
' ',
Style::new().bg(self.selected_style.background()),
);
self.selected_style
} else {
self.item_style
};
let text = truncate_to_cols(item, area.width_usize());
surface.print((area.left(), y), text, style);
}
}
}
#[cfg(test)]
mod tests {
use retroglyph_core::{Grid, Pos};
use super::*;
#[test]
fn list_widget_highlights_the_selected_item() {
let area = Rect::new(0, 0, 20, 2);
let items = ["Alpha", "Bravo"];
let list = List::new(&items);
let mut grid = Grid::new(20, 2);
let mut state = ListState::new();
state.select(Some(1));
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
let highlighted_bg = grid[Pos::new(0, 1)].style().background();
let plain_bg = grid[Pos::new(0, 0)].style().background();
assert_ne!(highlighted_bg, plain_bg);
}
#[test]
fn list_widget_highlights_nothing_when_unselected() {
let area = Rect::new(0, 0, 20, 2);
let items = ["Alpha", "Bravo"];
let list = List::new(&items);
let mut grid = Grid::new(20, 2);
let mut state = ListState::new();
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
let row0_bg = grid[Pos::new(0, 0)].style().background();
let row1_bg = grid[Pos::new(0, 1)].style().background();
assert_eq!(row0_bg, row1_bg);
}
fn items<'a>(names: &[&'a str]) -> Vec<&'a str> {
names.to_vec()
}
#[test]
fn scroll_offset_renders_the_window_starting_at_offset() {
let area = Rect::new(0, 0, 20, 2);
let names = items(&["Alpha", "Bravo", "Charlie", "Delta"]);
let list = List::new(&names);
let mut grid = Grid::new(20, 2);
let mut state = ListState::new();
state.set_offset(2); list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'C');
assert_eq!(grid[Pos::new(0, 1)].glyph(), 'D');
}
#[test]
fn selection_scrolled_out_of_view_highlights_nothing() {
let area = Rect::new(0, 0, 20, 2);
let names = items(&["Alpha", "Bravo", "Charlie", "Delta"]);
let list = List::new(&names);
let mut grid = Grid::new(20, 2);
let mut state = ListState::new();
state.select(Some(0)); state.set_offset(2); list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
let row0_bg = grid[Pos::new(0, 0)].style().background();
let row1_bg = grid[Pos::new(0, 1)].style().background();
assert_eq!(row0_bg, row1_bg); }
#[test]
fn item_style_can_be_overridden() {
let area = Rect::new(0, 0, 20, 1);
let items = ["Alpha"];
let custom = Style::new().fg(Color::RED);
let list = List::new(&items).item_style(custom);
let mut grid = Grid::new(20, 1);
let mut state = ListState::new();
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::RED);
}
#[test]
fn selected_style_can_be_overridden() {
let area = Rect::new(0, 0, 20, 1);
let items = ["Alpha"];
let custom = Style::new().fg(Color::GREEN).bg(Color::BLUE);
let list = List::new(&items).selected_style(custom);
let mut grid = Grid::new(20, 1);
let mut state = ListState::new();
state.select(Some(0));
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::GREEN);
assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::BLUE);
}
#[test]
fn clips_long_items_to_area_width() {
let area = Rect::new(0, 0, 5, 1);
let items = ["a much longer item than fits"];
let list = List::new(&items);
let mut grid = Grid::new(5, 1);
let mut state = ListState::new();
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(4, 0)].glyph(), 'c'); }
#[test]
fn ensure_visible_before_render_keeps_selection_on_screen() {
let area = Rect::new(0, 0, 20, 2); let names = items(&["Alpha", "Bravo", "Charlie", "Delta"]);
let list = List::new(&names);
let mut grid = Grid::new(20, 2);
let mut state = ListState::new();
state.select(Some(3)); state.ensure_visible(2);
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(0, 1)].glyph(), 'D');
let highlighted_bg = grid[Pos::new(0, 1)].style().background();
let plain_bg = grid[Pos::new(0, 0)].style().background();
assert_ne!(highlighted_bg, plain_bg);
}
#[test]
fn zero_height_is_a_no_op() {
let area = Rect::new(0, 0, 20, 0);
let items = ["Alpha"];
let list = List::new(&items);
let mut grid = Grid::new(20, 1);
let mut state = ListState::new();
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
}
#[test]
fn theme_maps_named_roles_onto_item_and_selected_styles() {
let area = Rect::new(0, 0, 20, 2);
let items = ["Alpha", "Bravo"];
let list = List::new(&items).theme(Theme::DARK);
let mut grid = Grid::new(20, 2);
let mut state = ListState::new();
state.select(Some(1));
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.fg);
assert_eq!(
grid[Pos::new(0, 0)].style().background(),
Theme::DARK.panel_bg
);
assert_eq!(grid[Pos::new(0, 1)].style().foreground(), Theme::DARK.bg);
assert_eq!(
grid[Pos::new(0, 1)].style().background(),
Theme::DARK.accent
);
}
#[test]
fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
let area = Rect::new(0, 0, 20, 1);
let items = ["Alpha"];
let list = List::new(&items).theme_on(Theme::DARK, Color::Default);
let mut grid = Grid::new(20, 1);
let mut state = ListState::new();
list.render(area, &mut Surface::new(&mut grid, area, 0), &mut state);
assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.fg);
assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::Default);
}
}