use std::{
cell::RefCell,
cmp::{max, min},
hash::{DefaultHasher, Hash, Hasher},
rc::Rc,
};
use crate::{
buffer::Buffer,
enums::Color,
geometry::Padding,
prelude::{MouseButton, MouseEvent, Rect, Vec2},
style::Style,
term::backend::MouseEventKind,
text::Text,
widgets::{Element, EventResult, LayoutNode, Span, ToSpan, Widget},
};
type ListHandler<M> = Box<dyn Fn(usize) -> M>;
pub struct List<M: 'static = ()> {
items: Vec<String>,
state: Rc<RefCell<ListState>>,
auto_scroll: bool,
handle_scroll: bool,
scroll_step: usize,
style: Style,
sel_style: Style,
highlight: String,
highlight_style: Style,
force_scrollbar: bool,
scrollbar_fg: Color,
thumb_fg: Color,
handlers: Vec<(MouseButton, ListHandler<M>)>,
on_scroll: Option<Box<dyn Fn(isize) -> M>>,
}
#[derive(Debug, Default, Hash)]
pub struct ListState {
pub offset: usize,
pub selected: Option<usize>,
}
impl<M> List<M> {
#[must_use]
pub fn new<T>(items: T, state: Rc<RefCell<ListState>>) -> Self
where
T: IntoIterator,
T::Item: AsRef<str>,
{
let items =
items.into_iter().map(|i| i.as_ref().to_string()).collect();
Self {
items,
state,
auto_scroll: false,
handle_scroll: true,
scroll_step: 1,
style: Default::default(),
sel_style: Default::default(),
highlight: String::new(),
highlight_style: Default::default(),
force_scrollbar: false,
scrollbar_fg: Color::Default,
thumb_fg: Color::Default,
handlers: vec![],
on_scroll: None,
}
}
#[must_use]
pub fn selected<T>(self, current: T) -> Self
where
T: Into<Option<usize>>,
{
self.state.borrow_mut().selected = current.into();
self
}
#[must_use]
pub fn auto_scroll(mut self) -> Self {
self.auto_scroll = true;
self
}
#[must_use]
pub fn scrollable(mut self, enabled: bool) -> Self {
self.handle_scroll = enabled;
self
}
#[must_use]
pub fn scroll_step(mut self, size: usize) -> Self {
self.scroll_step = size;
self
}
#[must_use]
pub fn style<T>(mut self, style: T) -> Self
where
T: Into<Style>,
{
self.style = style.into();
self
}
#[must_use]
pub fn selected_style<T>(mut self, style: T) -> Self
where
T: Into<Style>,
{
self.sel_style = style.into();
self
}
#[must_use]
pub fn highlight_symbol<T>(mut self, sel_char: T) -> Self
where
T: AsRef<str>,
{
self.highlight = sel_char.as_ref().to_string();
self
}
#[must_use]
pub fn highlight_style<T>(mut self, style: T) -> Self
where
T: Into<Style>,
{
self.highlight_style = style.into();
self
}
#[must_use]
pub fn force_scrollbar(mut self) -> Self {
self.force_scrollbar = true;
self
}
#[must_use]
pub fn scrollbar_fg(mut self, fg: Color) -> Self {
self.scrollbar_fg = fg;
self
}
#[must_use]
pub fn thumb_fg(mut self, fg: Color) -> Self {
self.thumb_fg = fg;
self
}
#[must_use]
pub fn on_click<F>(self, response: F) -> Self
where
F: Fn(usize) -> M + 'static,
{
self.on_press(MouseButton::Left, response)
}
#[must_use]
pub fn on_press<F>(mut self, button: MouseButton, response: F) -> Self
where
F: Fn(usize) -> M + 'static,
{
self.handlers.retain(|(b, _)| *b != button);
self.handlers.push((button, Box::new(response)));
self
}
#[must_use]
pub fn on_scroll<F>(mut self, response: F) -> Self
where
F: Fn(isize) -> M + 'static,
{
self.on_scroll = Some(Box::new(response));
self
}
}
impl ListState {
#[must_use]
pub fn new(offset: usize) -> Self {
Self {
offset,
selected: None,
}
}
#[must_use]
pub fn selected(offset: usize, selected: usize) -> Self {
Self {
offset,
selected: Some(selected),
}
}
}
impl<M: Clone + 'static> Widget<M> for List<M> {
fn render(&self, buffer: &mut Buffer, layout: &LayoutNode) {
let rect = layout.area;
let mut text_pos =
Vec2::new(rect.x() + self.highlight.len(), rect.y());
let mut text_size =
Vec2::new(rect.width() - self.highlight.len(), rect.height());
let has_bar = self.force_scrollbar || !self.fits(&text_size);
if has_bar {
text_size.x = text_size.x.saturating_sub(1);
}
if self.auto_scroll {
self.scroll_offset(&text_size);
}
if has_bar {
self.render_scrollbar(buffer, &rect);
}
let selected = self.state.borrow().selected;
for i in self.state.borrow().offset..self.items.len() {
let mut span = self.items[i].style(self.style);
if Some(i) == selected {
buffer.set_str_styled(
&self.highlight,
&Vec2::new(rect.x(), text_pos.y),
self.highlight_style,
);
span = self.items[i].style(self.sel_style);
}
let irect = Rect::from_coords(text_pos, text_size);
let res_pos = span.render_offset(buffer, irect, 0, None);
text_size.y = text_size.y.saturating_sub(res_pos.y - text_pos.y);
text_pos.y = res_pos.y + 1;
if rect.y() + rect.height() <= text_pos.y {
break;
}
text_size.y = rect.y() + rect.height() - text_pos.y;
}
}
fn height(&self, size: &Vec2) -> usize {
self.items
.iter()
.map(|i| <Span as Widget<M>>::height(&i.to_span(), size))
.sum()
}
fn width(&self, size: &Vec2) -> usize {
let mut width = 0;
let mut height = 0;
for item in self.items.iter() {
let span = item.to_span();
let h = <Span as Widget<M>>::height(&span, size);
width = max(
<Span as Widget<M>>::width(&span, &Vec2::new(size.x, h)),
width,
);
height += h;
}
width + self.highlight.len() + (height > size.y) as usize
}
fn layout_hash(&self) -> u64 {
let mut hasher = DefaultHasher::new();
self.items.hash(&mut hasher);
self.force_scrollbar.hash(&mut hasher);
self.highlight.hash(&mut hasher);
self.state.borrow().hash(&mut hasher);
hasher.finish()
}
fn on_event(&self, node: &LayoutNode, e: &MouseEvent) -> EventResult<M> {
if !node.area.contains_pos(&e.pos) {
return EventResult::None;
}
let mut area = node.area.inner(Padding::left(self.highlight.len()));
if self.force_scrollbar || !self.fits(area.size()) {
area.size.x -= 1;
}
for i in self.state.borrow().offset..self.items.len() {
let span: Element<M> = self.items[i].to_span().into();
let height = span.height(area.size());
let mut irect = Rect::from_coords(*area.pos(), *area.size());
irect.size.y = height;
if !irect.contains_pos(&e.pos) {
area = area.inner(Padding::top(height));
continue;
}
let m = self.item_event(e, i);
if !m.is_none() {
return m;
}
}
self.handle_mouse(e)
}
}
impl<M: Clone + 'static> List<M> {
fn render_scrollbar(&self, buffer: &mut Buffer, rect: &Rect) {
let rat = self.items.len() as f32 / rect.height() as f32;
let thumb_size = max(
1,
min((rect.height() as f32 / rat).round() as usize, rect.height()),
);
let mut thumb_offset = min(
(self.state.borrow().offset as f32 / rat) as usize,
rect.height() - thumb_size,
);
if let Some(selected) = self.state.borrow().selected
&& selected + 1 == self.items.len()
{
thumb_offset = rect.height() - thumb_size;
}
let x = (rect.x() + rect.width()).saturating_sub(1);
let mut bar_pos = Vec2::new(x, rect.y());
for _ in 0..rect.height() {
buffer[bar_pos].char('│').fg(self.scrollbar_fg);
bar_pos.y += 1;
}
bar_pos = Vec2::new(x, rect.y() + thumb_offset);
for _ in 0..thumb_size {
buffer[bar_pos].char('┃').fg(self.thumb_fg);
bar_pos.y += 1;
}
}
fn scroll_offset(&self, size: &Vec2) {
let Some(selected) = self.state.borrow().selected else {
return;
};
if selected < self.state.borrow().offset {
self.state.borrow_mut().offset = selected;
return;
}
while !self.is_visible(selected, self.state.borrow().offset, size) {
self.state.borrow_mut().offset += 1;
}
}
fn move_selection(&self, delta: isize) {
let mut state = self.state.borrow_mut();
let Some(selected) = state.selected else {
return;
};
let id = match delta < 0 {
true => selected.saturating_sub(delta.unsigned_abs()),
false => (selected + delta as usize)
.min(self.items.len().saturating_sub(1)),
};
state.selected = Some(id);
}
fn is_visible(&self, item: usize, offset: usize, size: &Vec2) -> bool {
let mut height = 0;
for i in offset..self.items.len() {
height +=
<Span as Widget<M>>::height(&self.items[i].to_span(), size);
if height > size.y {
return false;
}
if i == item {
return true;
}
}
false
}
fn fits(&self, size: &Vec2) -> bool {
self.is_visible(self.items.len() - 1, 0, size)
}
fn item_event(&self, event: &MouseEvent, id: usize) -> EventResult<M> {
match &event.kind {
MouseEventKind::Down(button) => self
.handlers
.iter()
.find(|(b, _)| b == button)
.map(|(_, m)| EventResult::Response(m(id)))
.unwrap_or(EventResult::None),
_ => EventResult::None,
}
}
fn handle_mouse(&self, event: &MouseEvent) -> EventResult<M> {
let delta = match &event.kind {
MouseEventKind::ScrollDown => self.scroll_step as isize,
MouseEventKind::ScrollUp => -(self.scroll_step as isize),
_ => return EventResult::None,
};
if let Some(handler) = &self.on_scroll {
return EventResult::Response(handler(delta));
}
if self.handle_scroll {
self.move_selection(delta);
return EventResult::Consumed;
}
EventResult::None
}
}
impl<M: Clone + 'static> From<List<M>> for Box<dyn Widget<M>> {
fn from(value: List<M>) -> Self {
Box::new(value)
}
}
impl<M: Clone + 'static> From<List<M>> for Element<M> {
fn from(value: List<M>) -> Self {
Element::new(value)
}
}