mirror of https://github.com/helix-editor/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
250 lines
6.9 KiB
Rust
250 lines
6.9 KiB
Rust
use crate::{
|
|
buffer::Buffer,
|
|
layout::{Corner, Rect},
|
|
style::Style,
|
|
text::Text,
|
|
widgets::{Block, StatefulWidget, Widget},
|
|
};
|
|
use std::iter::{self, Iterator};
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ListState {
|
|
offset: usize,
|
|
selected: Option<usize>,
|
|
}
|
|
|
|
impl Default for ListState {
|
|
fn default() -> ListState {
|
|
ListState {
|
|
offset: 0,
|
|
selected: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ListState {
|
|
pub fn selected(&self) -> Option<usize> {
|
|
self.selected
|
|
}
|
|
|
|
pub fn select(&mut self, index: Option<usize>) {
|
|
self.selected = index;
|
|
if index.is_none() {
|
|
self.offset = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct ListItem<'a> {
|
|
content: Text<'a>,
|
|
style: Style,
|
|
}
|
|
|
|
impl<'a> ListItem<'a> {
|
|
pub fn new<T>(content: T) -> ListItem<'a>
|
|
where
|
|
T: Into<Text<'a>>,
|
|
{
|
|
ListItem {
|
|
content: content.into(),
|
|
style: Style::default(),
|
|
}
|
|
}
|
|
|
|
pub fn style(mut self, style: Style) -> ListItem<'a> {
|
|
self.style = style;
|
|
self
|
|
}
|
|
|
|
pub fn height(&self) -> usize {
|
|
self.content.height()
|
|
}
|
|
}
|
|
|
|
/// A widget to display several items among which one can be selected (optional)
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// # use helix_tui::widgets::{Block, Borders, List, ListItem};
|
|
/// # use helix_tui::style::{Style, Color, Modifier};
|
|
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")];
|
|
/// List::new(items)
|
|
/// .block(Block::default().title("List").borders(Borders::ALL))
|
|
/// .style(Style::default().fg(Color::White))
|
|
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
|
/// .highlight_symbol(">>");
|
|
/// ```
|
|
#[derive(Debug, Clone)]
|
|
pub struct List<'a> {
|
|
block: Option<Block<'a>>,
|
|
items: Vec<ListItem<'a>>,
|
|
/// Style used as a base style for the widget
|
|
style: Style,
|
|
start_corner: Corner,
|
|
/// Style used to render selected item
|
|
highlight_style: Style,
|
|
/// Symbol in front of the selected item (Shift all items to the right)
|
|
highlight_symbol: Option<&'a str>,
|
|
}
|
|
|
|
impl<'a> List<'a> {
|
|
pub fn new<T>(items: T) -> List<'a>
|
|
where
|
|
T: Into<Vec<ListItem<'a>>>,
|
|
{
|
|
List {
|
|
block: None,
|
|
style: Style::default(),
|
|
items: items.into(),
|
|
start_corner: Corner::TopLeft,
|
|
highlight_style: Style::default(),
|
|
highlight_symbol: None,
|
|
}
|
|
}
|
|
|
|
pub fn block(mut self, block: Block<'a>) -> List<'a> {
|
|
self.block = Some(block);
|
|
self
|
|
}
|
|
|
|
pub fn style(mut self, style: Style) -> List<'a> {
|
|
self.style = style;
|
|
self
|
|
}
|
|
|
|
pub fn highlight_symbol(mut self, highlight_symbol: &'a str) -> List<'a> {
|
|
self.highlight_symbol = Some(highlight_symbol);
|
|
self
|
|
}
|
|
|
|
pub fn highlight_style(mut self, style: Style) -> List<'a> {
|
|
self.highlight_style = style;
|
|
self
|
|
}
|
|
|
|
pub fn start_corner(mut self, corner: Corner) -> List<'a> {
|
|
self.start_corner = corner;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<'a> StatefulWidget for List<'a> {
|
|
type State = ListState;
|
|
|
|
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
|
buf.set_style(area, self.style);
|
|
let list_area = match self.block.take() {
|
|
Some(b) => {
|
|
let inner_area = b.inner(area);
|
|
b.render(area, buf);
|
|
inner_area
|
|
}
|
|
None => area,
|
|
};
|
|
|
|
if list_area.width < 1 || list_area.height < 1 {
|
|
return;
|
|
}
|
|
|
|
if self.items.is_empty() {
|
|
return;
|
|
}
|
|
let list_height = list_area.height as usize;
|
|
|
|
let mut start = state.offset;
|
|
let mut end = state.offset;
|
|
let mut height = 0;
|
|
for item in self.items.iter().skip(state.offset) {
|
|
if height + item.height() > list_height {
|
|
break;
|
|
}
|
|
height += item.height();
|
|
end += 1;
|
|
}
|
|
|
|
let selected = state.selected.unwrap_or(0).min(self.items.len() - 1);
|
|
while selected >= end {
|
|
height = height.saturating_add(self.items[end].height());
|
|
end += 1;
|
|
while height > list_height {
|
|
height = height.saturating_sub(self.items[start].height());
|
|
start += 1;
|
|
}
|
|
}
|
|
while selected < start {
|
|
start -= 1;
|
|
height = height.saturating_add(self.items[start].height());
|
|
while height > list_height {
|
|
end -= 1;
|
|
height = height.saturating_sub(self.items[end].height());
|
|
}
|
|
}
|
|
state.offset = start;
|
|
|
|
let highlight_symbol = self.highlight_symbol.unwrap_or("");
|
|
let blank_symbol = iter::repeat(" ")
|
|
.take(highlight_symbol.width())
|
|
.collect::<String>();
|
|
|
|
let mut current_height = 0;
|
|
let has_selection = state.selected.is_some();
|
|
for (i, item) in self
|
|
.items
|
|
.iter_mut()
|
|
.enumerate()
|
|
.skip(state.offset)
|
|
.take(end - start)
|
|
{
|
|
let (x, y) = match self.start_corner {
|
|
Corner::BottomLeft => {
|
|
current_height += item.height() as u16;
|
|
(list_area.left(), list_area.bottom() - current_height)
|
|
}
|
|
_ => {
|
|
let pos = (list_area.left(), list_area.top() + current_height);
|
|
current_height += item.height() as u16;
|
|
pos
|
|
}
|
|
};
|
|
let area = Rect {
|
|
x,
|
|
y,
|
|
width: list_area.width,
|
|
height: item.height() as u16,
|
|
};
|
|
let item_style = self.style.patch(item.style);
|
|
buf.set_style(area, item_style);
|
|
|
|
let is_selected = state.selected.map(|s| s == i).unwrap_or(false);
|
|
let elem_x = if has_selection {
|
|
let symbol = if is_selected {
|
|
highlight_symbol
|
|
} else {
|
|
&blank_symbol
|
|
};
|
|
let (x, _) = buf.set_stringn(x, y, symbol, list_area.width as usize, item_style);
|
|
x
|
|
} else {
|
|
x
|
|
};
|
|
let max_element_width = (list_area.width - (elem_x - x)) as usize;
|
|
for (j, line) in item.content.lines.iter().enumerate() {
|
|
buf.set_spans(elem_x, y + j as u16, line, max_element_width as u16);
|
|
}
|
|
if is_selected {
|
|
buf.set_style(area, self.highlight_style);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> Widget for List<'a> {
|
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
|
let mut state = ListState::default();
|
|
StatefulWidget::render(self, area, buf, &mut state);
|
|
}
|
|
}
|