Skip to content

Commit

Permalink
wip: allow list to render widgets
Browse files Browse the repository at this point in the history
  • Loading branch information
joshka committed May 3, 2023
1 parent bb6983f commit e56d8ea
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 26 deletions.
185 changes: 160 additions & 25 deletions src/widgets/list.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt::Debug;

use crate::{
buffer::Buffer,
layout::{Corner, Rect},
Expand All @@ -7,6 +9,8 @@ use crate::{
};
use unicode_width::UnicodeWidthStr;

use super::Paragraph;

#[derive(Debug, Clone, Default)]
pub struct ListState {
offset: usize,
Expand All @@ -26,16 +30,16 @@ impl ListState {
}
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug)]
pub struct ListItem<'a> {
content: Text<'a>,
content: WidgetWrapper<'a>,
style: Style,
}

impl<'a> ListItem<'a> {
pub fn new<T>(content: T) -> ListItem<'a>
where
T: Into<Text<'a>>,
T: Into<WidgetWrapper<'a>>,
{
ListItem {
content: content.into(),
Expand All @@ -55,6 +59,64 @@ impl<'a> ListItem<'a> {
pub fn width(&self) -> usize {
self.content.width()
}

pub fn render(&self, area: Rect, buffer: &mut Buffer) {
self.content.render(area, buffer);
}
}

pub struct WidgetWrapper<'a> {
widget: Box<dyn Widget + 'a>,
height: usize,
width: usize,
}

impl<'a> WidgetWrapper<'a> {
pub fn new(widget: impl Widget + 'a, height: usize, width: usize) -> Self {
Self {
widget: Box::new(widget),
height,
width,
}
}

fn height(&self) -> usize {
self.height
}

fn width(&self) -> usize {
self.width
}

fn render(&self, area: Rect, buffer: &mut Buffer) {
self.widget.render(area, buffer);
}
}

impl Debug for WidgetWrapper<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("WidgetWrapper")
.field("height", &self.height)
.field("width", &self.width)
.finish()
}
}

/// Create a new WidgetWrapper that uses a Paragraph to display the given Text.
///
/// Previously the `ListItem` took `Into<Text<'a>>` as a parameter, but now
/// adds the ability to render Widgets. This exists to ensure backwards
/// compatability with previous List versions.
impl<'a, T> From<T> for WidgetWrapper<'a>
where
T: Into<Text<'a>>,
{
fn from(value: T) -> Self {
let text = value.into();
let height = text.height();
let width = text.width();
Self::new(Paragraph::new(text), height, width)
}
}

/// A widget to display several items among which one can be selected (optional)
Expand All @@ -71,7 +133,7 @@ impl<'a> ListItem<'a> {
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone)]
#[derive(Debug)]
pub struct List<'a> {
block: Option<Block<'a>>,
items: Vec<ListItem<'a>>,
Expand Down Expand Up @@ -198,7 +260,6 @@ impl<'a> StatefulWidget for List<'a> {
state.offset = start;

let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = " ".repeat(highlight_symbol.width());

let mut current_height = 0;
let has_selection = state.selected.is_some();
Expand Down Expand Up @@ -230,29 +291,27 @@ impl<'a> StatefulWidget for List<'a> {
buf.set_style(area, item_style);

let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
for (j, line) in item.content.lines.iter().enumerate() {
// if the item is selected, we need to display the highlight symbol:
// - either for the first line of the item only,
// - or for each line of the item if the appropriate option is set
let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
highlight_symbol
} else {
&blank_symbol
};
let (elem_x, max_element_width) = if has_selection {
let (elem_x, _) = buf.set_stringn(
x,
y + j as u16,
symbol,
list_area.width as usize,
item_style,
);
(elem_x, (list_area.width - (elem_x - x)))
if is_selected {
let count = if self.repeat_highlight_symbol {
item.height()
} else {
(x, list_area.width)
1
};
buf.set_spans(elem_x, y + j as u16, line, max_element_width);
for n in 0..count as u16 {
buf.set_string(x, y + n, highlight_symbol, item_style);
}
}
let symbol_width = if has_selection {
self.highlight_symbol.map_or(0, |s| s.width()) as u16
} else {
0
};
let item_area = Rect {
x: area.x + symbol_width,
width: area.width - symbol_width,
..area
};
item.render(item_area, buf);
if is_selected {
buf.set_style(area, self.highlight_style);
}
Expand All @@ -266,3 +325,79 @@ impl<'a> Widget for List<'a> {
StatefulWidget::render(self, area, buf, &mut state);
}
}

#[cfg(test)]
mod tests {
// use crate::assert_buffer_eq;

use crate::{assert_buffer_eq, layout::Alignment, widgets::Borders};

use super::*;

#[test]
fn can_render_text_using_str() {
let list = List::new(vec![
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
]);
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 3));
Widget::render(&list, buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(vec!["Item 1", "Item 2", "Item 3"]));
}

#[test]
fn can_render_text_using_paragraphs() {
let paragraphs = vec![
Paragraph::new("Item 1"),
Paragraph::new("Item 2"),
Paragraph::new("Item 3"),
];
let items = paragraphs
.iter()
.map(|p| WidgetWrapper::new(p.clone(), 1, 15))
.map(|w| ListItem::new(w))
.collect::<Vec<ListItem>>();
let list = List::new(items);
let mut buf = Buffer::empty(Rect::new(0, 0, 6, 3));
Widget::render(&list, buf.area, &mut buf);
assert_buffer_eq!(buf, Buffer::with_lines(vec!["Item 1", "Item 2", "Item 3"]));
}

#[test]
fn can_render_blocks() {
let blocks = vec![
Block::default().title("Item 1").borders(Borders::ALL),
Block::default()
.title("Item 2")
.borders(Borders::ALL)
.title_alignment(Alignment::Center),
Block::default()
.title("Item 3")
.borders(Borders::ALL)
.title_alignment(Alignment::Right),
];
let items = blocks
.iter()
.map(|b| WidgetWrapper::new(b.clone(), 3, 15))
.map(|w| ListItem::new(w))
.collect::<Vec<ListItem>>();
let list = List::new(items);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 9));
Widget::render(&list, buf.area, &mut buf);
assert_buffer_eq!(
buf,
Buffer::with_lines(vec![
"┌Item 1───────┐",
"│ │",
"└─────────────┘",
"┌───Item 2────┐",
"│ │",
"└─────────────┘",
"┌───────Item 3┐",
"│ │",
"└─────────────┘",
])
);
}
}
2 changes: 1 addition & 1 deletion tests/widgets_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fn widgets_list_should_truncate_items() {
state.select(case.selected);
terminal
.draw(|f| {
let list = List::new(case.items.clone())
let list = List::new(case.items)
.block(Block::default().borders(Borders::RIGHT))
.highlight_symbol(">> ");
f.render_stateful_widget(list, Rect::new(0, 0, 8, 2), &mut state);
Expand Down

0 comments on commit e56d8ea

Please sign in to comment.