use crate::_private::NonExhaustive;
use crate::event::util::MouseFlags;
use crate::list::selection::{RowSelection, RowSetSelection};
use crate::text::HasScreenCursor;
use crate::util::{fallback_select_style, revert_style};
use rat_event::{HandleEvent, MouseOnly, Outcome, Regular};
use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
use rat_reloc::{RelocatableState, relocate_areas};
use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState, ScrollStyle};
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use ratatui_core::text::{Line, Text};
use ratatui_core::widgets::{StatefulWidget, Widget};
use ratatui_crossterm::crossterm::event::Event;
use ratatui_widgets::block::Block;
use ratatui_widgets::list::ListDirection;
use ratatui_widgets::table::HighlightSpacing;
use std::cmp::{max, min};
use std::collections::HashSet;
use std::marker::PhantomData;
pub mod edit;
pub trait ListSelection {
fn count(&self) -> usize;
fn is_selected(&self, n: usize) -> bool;
fn lead_selection(&self) -> Option<usize>;
fn scroll_selected(&self) -> bool {
false
}
#[allow(unused_variables)]
fn validate_rows(&mut self, rows: usize) {}
#[allow(unused_variables)]
fn items_added(&mut self, pos: usize, n: usize) {}
#[allow(unused_variables)]
fn items_removed(&mut self, pos: usize, n: usize, rows: usize) {}
}
#[derive(Debug, Default, Clone)]
pub struct List<'a, Selection = RowSelection> {
items: Vec<ListItem<'a>>,
style: Style,
block: Option<Block<'a>>,
scroll: Option<Scroll<'a>>,
scroll_padding: usize,
varying_heights: bool,
select_style: Option<Style>,
focus_style: Option<Style>,
direction: ListDirection,
highlight_spacing: HighlightSpacing,
highlight_symbol: Option<Line<'a>>,
repeat_highlight_symbol: bool,
highlight_style: Option<Style>,
_phantom: PhantomData<Selection>,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ListItem<'a> {
pub(crate) content: Text<'a>,
pub(crate) style: Style,
}
#[derive(Debug, Clone)]
pub struct ListStyle {
pub style: Style,
pub block: Option<Block<'static>>,
pub border_style: Option<Style>,
pub title_style: Option<Style>,
pub scroll: Option<ScrollStyle>,
pub scroll_padding: Option<usize>,
pub select: Option<Style>,
pub focus: Option<Style>,
pub highlight_spacing: Option<HighlightSpacing>,
pub highlight_symbol: Option<&'static str>,
pub repeat_highlight_symbol: Option<bool>,
pub highlight_style: Option<Style>,
pub non_exhaustive: NonExhaustive,
}
#[derive(Debug, Clone)]
pub enum RowHeights {
Uniform(u8),
Varying(Vec<u8>),
}
impl Default for RowHeights {
fn default() -> Self {
RowHeights::Uniform(1)
}
}
impl RowHeights {
pub(crate) fn clear(&mut self) {
match self {
RowHeights::Uniform(_) => {}
RowHeights::Varying(h) => h.clear(),
}
}
pub(crate) fn push(&mut self, row_height: u8, varying: bool) {
if varying {
match self {
RowHeights::Uniform(_) => {
*self = RowHeights::Varying(vec![row_height]);
}
RowHeights::Varying(h) => h.push(row_height),
}
} else {
*self = RowHeights::Uniform(row_height);
}
}
pub(crate) fn items_added(&mut self, pos: usize, n: usize) {
match self {
RowHeights::Uniform(_) => {}
RowHeights::Varying(h) => {
if pos <= h.len() {
for _ in 0..n {
h.insert(pos, 1);
}
}
}
}
}
pub(crate) fn items_added_with_heights(&mut self, pos: usize, height: &[u8]) {
match self {
RowHeights::Uniform(_) => {}
RowHeights::Varying(h) => {
if pos <= h.len() {
h.splice(pos..pos, height.into_iter().copied());
}
}
}
}
pub(crate) fn items_removed(&mut self, pos: usize, n: usize) {
match self {
RowHeights::Uniform(_) => {}
RowHeights::Varying(h) => {
if pos < h.len() {
if pos + n < h.len() {
h.drain(pos..pos + n);
} else {
h.drain(pos..);
}
}
}
}
}
}
#[derive(Debug)]
pub struct ListState<Selection = RowSelection> {
pub area: Rect,
pub inner: Rect,
pub row_areas: Vec<Rect>,
pub row_heights: RowHeights,
pub scroll_padding: usize,
pub direction: ListDirection,
pub rows: usize,
pub scroll: ScrollState,
pub focus: FocusFlag,
pub selection: Selection,
pub mouse: MouseFlags,
pub non_exhaustive: NonExhaustive,
}
impl Default for ListStyle {
fn default() -> Self {
Self {
style: Default::default(),
block: Default::default(),
border_style: Default::default(),
title_style: Default::default(),
scroll: Default::default(),
scroll_padding: Default::default(),
select: Default::default(),
focus: Default::default(),
highlight_spacing: Default::default(),
highlight_symbol: Default::default(),
repeat_highlight_symbol: Default::default(),
highlight_style: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<'a> ListItem<'a> {
pub fn new<T>(content: T) -> Self
where
T: Into<Text<'a>>,
{
Self {
content: content.into(),
style: Style::default(),
}
}
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
pub fn height(&self) -> usize {
self.content.height()
}
pub fn width(&self) -> usize {
self.content.width()
}
}
impl<'a, T> From<T> for ListItem<'a>
where
T: Into<Text<'a>>,
{
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<'a, Selection> List<'a, Selection> {
pub fn new<T>(items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
let items = items.into_iter().map(|v| v.into()).collect();
Self {
block: None,
scroll: None,
items,
style: Default::default(),
select_style: Default::default(),
focus_style: Default::default(),
direction: Default::default(),
highlight_spacing: Default::default(),
highlight_symbol: Default::default(),
repeat_highlight_symbol: Default::default(),
highlight_style: Default::default(),
scroll_padding: 0,
varying_heights: Default::default(),
_phantom: Default::default(),
}
}
pub fn items<T>(mut self, items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
let items = items.into_iter().map(|v| v.into()).collect();
self.items = items;
self
}
#[inline]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self.block = self.block.map(|v| v.style(self.style));
self
}
#[inline]
pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
self.scroll = Some(scroll);
self
}
#[inline]
pub fn styles_opt(self, styles: Option<ListStyle>) -> Self {
if let Some(styles) = styles {
self.styles(styles)
} else {
self
}
}
#[inline]
pub fn styles(mut self, styles: ListStyle) -> Self {
self.style = styles.style;
if styles.block.is_some() {
self.block = styles.block;
}
if let Some(border_style) = styles.border_style {
self.block = self.block.map(|v| v.border_style(border_style));
}
if let Some(title_style) = styles.title_style {
self.block = self.block.map(|v| v.title_style(title_style));
}
self.block = self.block.map(|v| v.style(self.style));
if let Some(styles) = styles.scroll {
self.scroll = self.scroll.map(|v| v.styles(styles));
}
if let Some(scroll_padding) = styles.scroll_padding {
self.scroll_padding = scroll_padding;
}
if styles.select.is_some() {
self.select_style = styles.select;
}
if styles.focus.is_some() {
self.focus_style = styles.focus;
}
if let Some(highlight_spacing) = styles.highlight_spacing {
self.highlight_spacing = highlight_spacing;
}
if let Some(highlight_symbol) = styles.highlight_symbol {
self.highlight_symbol = Some(highlight_symbol.into());
}
if let Some(repeat_highlight_symbol) = styles.repeat_highlight_symbol {
self.repeat_highlight_symbol = repeat_highlight_symbol;
}
if let Some(highlight_style) = styles.highlight_style {
self.highlight_style = Some(highlight_style);
}
self
}
#[inline]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self.block = self.block.map(|v| v.style(self.style));
self
}
#[inline]
pub fn select_style<S: Into<Style>>(mut self, select_style: S) -> Self {
self.select_style = Some(select_style.into());
self
}
#[inline]
pub fn focus_style<S: Into<Style>>(mut self, focus_style: S) -> Self {
self.focus_style = Some(focus_style.into());
self
}
pub fn varying_heights(mut self, varying: bool) -> Self {
self.varying_heights = varying;
self
}
#[inline]
pub fn direction(mut self, direction: ListDirection) -> Self {
self.direction = direction;
self
}
#[inline]
pub fn highlight_symbol<L: Into<Line<'a>>>(mut self, highlight_symbol: L) -> Self {
self.highlight_symbol = Some(highlight_symbol.into());
self
}
#[inline]
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
self.highlight_style = Some(style.into());
self
}
#[inline]
pub fn repeat_highlight_symbol(mut self, repeat: bool) -> Self {
self.repeat_highlight_symbol = repeat;
self
}
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
pub fn scroll_padding(mut self, padding: usize) -> Self {
self.scroll_padding = padding;
self
}
#[inline]
pub fn len(&self) -> usize {
self.items.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn width(&self) -> u16 {
let mut width = 0;
for item in &self.items {
width = max(width, item.width() as u16);
}
width
}
pub fn height(&self) -> u16 {
let mut height = 0u16;
for item in &self.items {
height += height.saturating_add(item.height() as u16);
}
height
}
}
impl<'a, Item, Selection> FromIterator<Item> for List<'a, Selection>
where
Item: Into<ListItem<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
Self::new(iter)
}
}
impl<Selection: ListSelection> StatefulWidget for List<'_, Selection> {
type State = ListState<Selection>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
render_list(self, area, buf, state)
}
}
fn render_list<Selection: ListSelection>(
widget: List<'_, Selection>,
area: Rect,
buf: &mut Buffer,
state: &mut ListState<Selection>,
) {
state.rows = widget.items.len();
state.area = area;
state.selection.validate_rows(state.rows);
state.scroll_padding = widget.scroll_padding;
state.direction = widget.direction;
let sa = ScrollArea::new()
.style(widget.style)
.block(widget.block.as_ref())
.v_scroll(widget.scroll.as_ref());
state.inner = sa.inner(area, None, Some(&state.scroll));
sa.render(
area,
buf,
&mut ScrollAreaState::new().v_scroll(&mut state.scroll),
);
let mut n = 0;
let mut height = 0;
for item in widget.items.iter().rev() {
height += item.height();
if height > state.inner.height as usize {
break;
}
n += 1;
}
state.scroll.set_max_offset(state.rows.saturating_sub(n));
let (high_area, item_area) = layout_columns(&widget, state.inner, state);
let focus_style = widget.focus_style.unwrap_or(revert_style(widget.style));
let select_style = widget
.select_style
.unwrap_or(fallback_select_style(widget.style));
let (style, select_style) = if state.is_focused() {
(widget.style, focus_style)
} else {
(widget.style, select_style)
};
let high_style = if let Some(highlight_style) = widget.highlight_style {
style.patch(highlight_style)
} else {
style
};
let high_sym = if let Some(high_sym) = widget.highlight_symbol {
high_sym
} else {
Line::default()
};
state.row_areas.clear();
state.row_heights.clear();
state.scroll.page_len = 0;
if widget.direction == ListDirection::TopToBottom {
let mut row_y = state.inner.y;
for (row, item) in widget.items.into_iter().enumerate() {
let row_height = item.height() as u8;
state.row_heights.push(row_height, widget.varying_heights);
if row < state.offset() || row_y >= state.inner.bottom() {
continue;
}
let row_area = Rect::new(state.inner.x, row_y, state.inner.width, row_height as u16)
.intersection(state.inner);
state.row_areas.push(row_area);
state.scroll.page_len += 1;
let style = if state.selection.is_selected(row) {
style.patch(select_style).patch(item.style)
} else {
style.patch(item.style)
};
buf.set_style(row_area, style);
buf.set_style(row_area, item.content.style);
for mut line in item.content.lines.into_iter() {
if row_y >= state.inner.bottom() {
break;
}
line.alignment = line.alignment.or(item.content.alignment);
line.render(
Rect::new(row_area.x + item_area.x, row_y, item_area.width, 1),
buf,
);
row_y += 1;
}
if state.selection.is_selected(row) {
for yy in row_area.y..row_area.bottom() {
let high_area = Rect::new(
row_area.x + high_area.x, yy,
high_area.width,
1,
);
if widget.repeat_highlight_symbol || yy == row_area.y {
buf.set_style(high_area, high_style);
(&high_sym).render(high_area, buf);
}
}
}
}
} else if widget.direction == ListDirection::BottomToTop {
let mut row_y = state.inner.bottom();
for (row, item) in widget.items.into_iter().enumerate() {
let row_height = item.height() as u8;
state.row_heights.push(row_height, widget.varying_heights);
if row < state.offset() || row_y <= state.inner.y {
continue;
}
let row_area = Rect::new(
state.inner.x,
row_y.saturating_sub(row_height as u16),
state.inner.width,
row_height as u16,
)
.intersection(state.inner);
state.row_areas.push(row_area);
state.scroll.page_len += 1;
let style = if state.selection.is_selected(row) {
style.patch(select_style).patch(item.style)
} else {
style.patch(item.style)
};
buf.set_style(row_area, style);
buf.set_style(row_area, item.content.style);
for mut line in item.content.lines.into_iter().rev() {
if row_y <= state.inner.y {
break;
}
row_y -= 1;
line.alignment = line.alignment.or(item.content.alignment);
line.render(
Rect::new(row_area.x + item_area.x, row_y, item_area.width, 1),
buf,
);
}
if state.selection.is_selected(row) {
for yy in row_area.y..row_area.bottom() {
let high_area = Rect::new(
row_area.x + high_area.x, yy,
high_area.width,
1,
);
if widget.repeat_highlight_symbol || yy == row_area.y {
buf.set_style(high_area, high_style);
(&high_sym).render(high_area, buf);
}
}
}
}
}
}
fn layout_columns<Selection: ListSelection>(
widget: &List<'_, Selection>,
area: Rect,
state: &ListState<Selection>,
) -> (Rect, Rect) {
let is_highlight = match widget.highlight_spacing {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => state.selection.lead_selection().is_some(),
HighlightSpacing::Never => false,
};
let highlight_area = if let Some(highlight_symbol) = &widget.highlight_symbol
&& is_highlight
{
Rect::new(0, 0, highlight_symbol.width() as u16, 0)
} else {
Rect::new(0, 0, 0, 0)
};
let item_area = Rect::new(
highlight_area.right(),
0,
area.width.saturating_sub(highlight_area.right()),
0,
);
(highlight_area, item_area)
}
impl<Selection> HasFocus for ListState<Selection> {
fn build(&self, builder: &mut FocusBuilder) {
builder.leaf_widget(self);
}
#[inline]
fn focus(&self) -> FocusFlag {
self.focus.clone()
}
#[inline]
fn area(&self) -> Rect {
self.area
}
}
impl<Selection> HasScreenCursor for ListState<Selection> {
fn screen_cursor(&self) -> Option<(u16, u16)> {
None
}
}
impl<Selection: Default> Default for ListState<Selection> {
fn default() -> Self {
Self {
area: Default::default(),
inner: Default::default(),
row_areas: Default::default(),
row_heights: Default::default(),
scroll_padding: Default::default(),
direction: Default::default(),
rows: Default::default(),
scroll: Default::default(),
focus: Default::default(),
selection: Default::default(),
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<Selection: Clone> Clone for ListState<Selection> {
fn clone(&self) -> Self {
Self {
area: self.area,
inner: self.inner,
row_areas: self.row_areas.clone(),
row_heights: self.row_heights.clone(),
scroll_padding: self.scroll_padding,
direction: self.direction,
rows: self.rows,
scroll: self.scroll.clone(),
focus: self.focus.new_instance(),
selection: self.selection.clone(),
mouse: Default::default(),
non_exhaustive: NonExhaustive,
}
}
}
impl<Selection> RelocatableState for ListState<Selection> {
fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
self.area.relocate(shift, clip);
self.inner.relocate(shift, clip);
relocate_areas(self.row_areas.as_mut_slice(), shift, clip);
self.scroll.relocate(shift, clip);
}
}
impl<Selection: ListSelection> ListState<Selection> {
pub fn new() -> Self
where
Selection: Default,
{
Default::default()
}
pub fn named(name: &str) -> Self
where
Selection: Default,
{
let mut z = Self::default();
z.focus = z.focus.with_name(name);
z
}
#[inline]
pub fn rows(&self) -> usize {
self.rows
}
pub fn items_added(&mut self, pos: usize, n: usize) {
self.rows = self.rows.saturating_add(n);
self.scroll.items_added(pos, n);
self.row_heights.items_added(pos, n);
self.selection.items_added(pos, n);
}
pub fn items_added_with_heights(&mut self, pos: usize, height: &[u8]) {
self.rows = self.rows.saturating_add(height.len());
self.scroll.items_added(pos, height.len());
self.row_heights.items_added_with_heights(pos, height);
self.selection.items_added(pos, height.len());
}
pub fn items_removed(&mut self, pos: usize, n: usize) {
self.rows = self.rows.saturating_sub(n);
self.scroll.items_removed(pos, n);
self.row_heights.items_removed(pos, n);
self.selection.items_removed(pos, n, self.rows);
}
pub fn rows_changed(&mut self, rows: usize) {
self.selection.validate_rows(rows);
self.scroll.rows_changed(rows);
self.row_heights.clear();
self.rows = rows;
}
#[inline]
pub fn clear_offset(&mut self) {
self.scroll.set_offset(0);
}
#[inline]
pub fn max_offset(&self) -> usize {
self.scroll.max_offset()
}
#[inline]
pub fn set_max_offset(&mut self, max: usize) {
self.scroll.set_max_offset(max);
}
#[inline]
pub fn offset(&self) -> usize {
self.scroll.offset()
}
#[inline]
pub fn set_offset(&mut self, offset: usize) -> bool {
self.scroll.set_offset(offset)
}
#[inline]
pub fn page_len(&self) -> usize {
self.scroll.page_len()
}
pub fn scroll_by(&self) -> usize {
self.scroll.scroll_by()
}
#[inline]
pub fn scroll_to_selected(&mut self) -> bool {
if let Some(selected) = self.selection.lead_selection() {
self.scroll_to(selected)
} else {
false
}
}
#[inline]
pub fn scroll_to(&mut self, mut pos: usize) -> bool {
if pos >= self.rows {
false
} else if self.inner.is_empty() {
self.set_offset(pos)
} else if let RowHeights::Uniform(row_height) = &self.row_heights {
let page_len = self.inner.height.div_ceil(*row_height as u16) as usize;
let scroll_padding = min((page_len - 1) / 2, self.scroll_padding);
if pos
>= self
.offset()
.saturating_add(page_len - 1)
.saturating_sub(scroll_padding)
{
if page_len as u16 * (*row_height) as u16 > self.inner.height {
self.set_offset(pos.saturating_sub(page_len - 2))
} else {
self.set_offset(pos.saturating_sub(page_len - 1))
}
} else if pos < self.offset() {
self.set_offset(pos.saturating_sub(scroll_padding))
} else {
false
}
} else if let RowHeights::Varying(row_heights) = &self.row_heights {
let scroll_padding = min((self.page_len() - 1) / 2, self.scroll_padding);
if pos
>= self
.offset()
.saturating_add(self.page_len() - 1)
.saturating_sub(scroll_padding)
{
if pos >= row_heights.len() {
self.set_offset(pos)
} else {
pos = min(pos + scroll_padding, row_heights.len().saturating_sub(1));
let mut sum_height = row_heights[pos] as u16;
if sum_height < self.inner.height {
loop {
if pos < 1 {
break;
}
sum_height += row_heights[pos - 1] as u16;
if sum_height > self.inner.height {
break;
}
pos -= 1;
}
}
self.set_offset(pos)
}
} else if pos < self.offset().saturating_add(scroll_padding) {
self.set_offset(pos.saturating_sub(scroll_padding))
} else {
false
}
} else {
false
}
}
#[inline]
pub fn scroll_up(&mut self, n: usize) -> bool {
if self.direction == ListDirection::TopToBottom {
self.scroll.scroll_up(n)
} else {
self.scroll.scroll_down(n)
}
}
#[inline]
pub fn scroll_down(&mut self, n: usize) -> bool {
if self.direction == ListDirection::TopToBottom {
self.scroll.scroll_down(n)
} else {
self.scroll.scroll_up(n)
}
}
}
impl<Selection: ListSelection> ListState<Selection> {
pub fn row_area(&self, row: usize) -> Option<Rect> {
if row < self.scroll.offset() || row >= self.scroll.offset() + self.scroll.page_len() {
return None;
}
let row = row - self.scroll.offset;
if row >= self.row_areas.len() {
None
} else {
Some(self.row_areas[row])
}
}
#[inline]
pub fn row_at_clicked(&self, pos: (u16, u16)) -> Option<usize> {
self.mouse
.row_at(&self.row_areas, pos.1)
.map(|v| self.scroll.offset() + v)
}
#[inline]
pub fn row_at_drag(&self, pos: (u16, u16)) -> usize {
match self.mouse.row_at_drag(self.inner, &self.row_areas, pos.1) {
Ok(v) => self.scroll.offset() + v,
Err(v) if v <= 0 => self.scroll.offset().saturating_sub((-v) as usize),
Err(v) => self.scroll.offset() + self.row_areas.len() + v as usize,
}
}
}
impl ListState<RowSelection> {
#[inline]
pub fn set_scroll_selection(&mut self, scroll: bool) {
self.selection.set_scroll_selected(scroll);
}
pub(crate) fn remap_offset_selection(&self, offset: usize) -> usize {
if self.scroll.max_offset() > 0 {
(self.rows * offset) / self.scroll.max_offset()
} else {
0 }
}
#[inline]
pub fn clear_selection(&mut self) {
self.selection.clear();
}
#[inline]
pub fn has_selection(&mut self) -> bool {
self.selection.has_selection()
}
#[inline]
pub fn selected(&self) -> Option<usize> {
self.selection.lead_selection()
}
#[inline]
pub fn selected_checked(&self) -> Option<usize> {
self.selection.lead_selection().filter(|v| *v < self.rows)
}
#[inline]
pub fn select(&mut self, row: Option<usize>) -> bool {
self.selection.select(row)
}
#[inline]
pub fn move_to(&mut self, row: usize) -> bool {
let r = self.selection.move_to(row, self.rows.saturating_sub(1));
let s = self.scroll_to(self.selection.selected().expect("row"));
r || s
}
#[inline]
pub fn move_up(&mut self, n: usize) -> bool {
let r = if self.direction == ListDirection::TopToBottom {
self.selection.move_up(n, self.rows.saturating_sub(1))
} else {
self.selection.move_down(n, self.rows.saturating_sub(1))
};
let s = self.scroll_to(self.selection.selected().expect("row"));
r || s
}
#[inline]
pub fn move_down(&mut self, n: usize) -> bool {
let r = if self.direction == ListDirection::TopToBottom {
self.selection.move_down(n, self.rows.saturating_sub(1))
} else {
self.selection.move_up(n, self.rows.saturating_sub(1))
};
let s = self.scroll_to(self.selection.selected().expect("row"));
r || s
}
}
impl ListState<RowSetSelection> {
#[inline]
pub fn clear_selection(&mut self) {
self.selection.clear();
}
#[inline]
pub fn has_selection(&mut self) -> bool {
self.selection.has_selection()
}
#[inline]
pub fn selected(&self) -> HashSet<usize> {
self.selection.selected()
}
#[inline]
pub fn set_lead(&mut self, row: Option<usize>, extend: bool) -> bool {
if let Some(row) = row {
self.selection
.set_lead(Some(min(row, self.rows.saturating_sub(1))), extend)
} else {
self.selection.set_lead(None, extend)
}
}
#[inline]
pub fn lead(&self) -> Option<usize> {
self.selection.lead()
}
#[inline]
pub fn anchor(&self) -> Option<usize> {
self.selection.anchor()
}
#[inline]
pub fn set_lead_clamped(&mut self, lead: usize, max: usize, extend: bool) {
self.selection.move_to(lead, max, extend);
}
#[inline]
pub fn retire_selection(&mut self) {
self.selection.retire_selection();
}
#[inline]
pub fn add_selected(&mut self, idx: usize) {
self.selection.add(idx);
}
#[inline]
pub fn remove_selected(&mut self, idx: usize) {
self.selection.remove(idx);
}
#[inline]
pub fn move_to(&mut self, row: usize, extend: bool) -> bool {
let r = self
.selection
.move_to(row, self.rows.saturating_sub(1), extend);
let s = self.scroll_to(self.selection.lead().expect("row"));
r || s
}
#[inline]
pub fn move_up(&mut self, n: usize, extend: bool) -> bool {
let r = if self.direction == ListDirection::TopToBottom {
self.selection
.move_up(n, self.rows.saturating_sub(1), extend)
} else {
self.selection
.move_down(n, self.rows.saturating_sub(1), extend)
};
let s = self.scroll_to(self.selection.lead().expect("row"));
r || s
}
#[inline]
pub fn move_down(&mut self, n: usize, extend: bool) -> bool {
let r = if self.direction == ListDirection::TopToBottom {
self.selection
.move_down(n, self.rows.saturating_sub(1), extend)
} else {
self.selection
.move_up(n, self.rows.saturating_sub(1), extend)
};
let s = self.scroll_to(self.selection.lead().expect("row"));
r || s
}
}
pub mod selection {
use crate::event::{HandleEvent, MouseOnly, Outcome, Regular, ct_event};
use crate::list::{ListSelection, ListState};
use rat_event::event_flow;
use rat_focus::HasFocus;
use rat_ftable::TableSelection;
use rat_scrolled::ScrollAreaState;
use rat_scrolled::event::ScrollOutcome;
use ratatui_crossterm::crossterm::event::{Event, KeyModifiers};
use std::cmp::max;
pub type NoSelection = rat_ftable::selection::NoSelection;
impl ListSelection for NoSelection {
fn count(&self) -> usize {
0
}
#[inline]
fn is_selected(&self, _n: usize) -> bool {
false
}
#[inline]
fn lead_selection(&self) -> Option<usize> {
None
}
fn validate_rows(&mut self, _rows: usize) {}
fn items_added(&mut self, _pos: usize, _n: usize) {}
fn items_removed(&mut self, _pos: usize, _n: usize, _rows: usize) {}
}
impl HandleEvent<Event, Regular, Outcome> for ListState<NoSelection> {
fn handle(&mut self, event: &Event, _keymap: Regular) -> Outcome {
let res = if self.is_focused() {
match event {
ct_event!(keycode press Down) => self.scroll_down(1).into(),
ct_event!(keycode press Up) => self.scroll_up(1).into(),
ct_event!(keycode press CONTROL-Down) | ct_event!(keycode press End) => {
self.scroll_to(self.max_offset()).into()
}
ct_event!(keycode press CONTROL-Up) | ct_event!(keycode press Home) => {
self.scroll_to(0).into()
}
ct_event!(keycode press PageUp) => {
self.scroll_up(self.page_len().saturating_sub(1)).into()
}
ct_event!(keycode press PageDown) => {
self.scroll_down(self.page_len().saturating_sub(1)).into()
}
_ => Outcome::Continue,
}
} else {
Outcome::Continue
};
if res == Outcome::Continue {
self.handle(event, MouseOnly)
} else {
res
}
}
}
impl HandleEvent<Event, MouseOnly, Outcome> for ListState<NoSelection> {
fn handle(&mut self, event: &Event, _keymap: MouseOnly) -> Outcome {
if self.is_obscured() {
return Outcome::Continue;
}
let mut sas = ScrollAreaState::new()
.area(self.inner)
.v_scroll(&mut self.scroll);
let r = match sas.handle(event, MouseOnly) {
ScrollOutcome::Up(v) => self.scroll_up(v),
ScrollOutcome::Down(v) => self.scroll_down(v),
ScrollOutcome::VPos(v) => self.set_offset(v),
ScrollOutcome::Left(_) => false,
ScrollOutcome::Right(_) => false,
ScrollOutcome::HPos(_) => false,
ScrollOutcome::Continue => false,
ScrollOutcome::Unchanged => false,
ScrollOutcome::Changed => true,
};
if r {
return Outcome::Changed;
}
Outcome::Unchanged
}
}
pub type RowSelection = rat_ftable::selection::RowSelection;
impl ListSelection for RowSelection {
fn count(&self) -> usize {
<Self as TableSelection>::count(self)
}
#[inline]
fn is_selected(&self, n: usize) -> bool {
<Self as TableSelection>::is_selected_row(self, n)
}
#[inline]
fn lead_selection(&self) -> Option<usize> {
<Self as TableSelection>::lead_selection(self).map(|(_, y)| y)
}
fn scroll_selected(&self) -> bool {
RowSelection::scroll_selected(self)
}
fn validate_rows(&mut self, rows: usize) {
<Self as TableSelection>::validate_rows(self, rows);
}
fn items_added(&mut self, pos: usize, n: usize) {
<Self as TableSelection>::items_added(self, pos, n);
}
fn items_removed(&mut self, pos: usize, n: usize, rows: usize) {
<Self as TableSelection>::items_removed(self, pos, n, rows);
}
}
impl HandleEvent<Event, Regular, Outcome> for ListState<RowSelection> {
fn handle(&mut self, event: &Event, _keymap: Regular) -> Outcome {
let res = if self.is_focused() {
match event {
ct_event!(keycode press Down) => self.move_down(1).into(),
ct_event!(keycode press Up) => self.move_up(1).into(),
ct_event!(keycode press CONTROL-Down) | ct_event!(keycode press End) => {
self.move_to(self.rows.saturating_sub(1)).into()
}
ct_event!(keycode press CONTROL-Up) | ct_event!(keycode press Home) => {
self.move_to(0).into()
}
ct_event!(keycode press PageUp) => self
.move_up(max(1, self.page_len().saturating_sub(1)))
.into(),
ct_event!(keycode press PageDown) => self
.move_down(max(1, self.page_len().saturating_sub(1)))
.into(),
_ => Outcome::Continue,
}
} else {
Outcome::Continue
};
if res == Outcome::Continue {
self.handle(event, MouseOnly)
} else {
res
}
}
}
impl HandleEvent<Event, MouseOnly, Outcome> for ListState<RowSelection> {
fn handle(&mut self, event: &Event, _keymap: MouseOnly) -> Outcome {
if self.is_obscured() {
return Outcome::Continue;
}
event_flow!(
return match event {
ct_event!(mouse any for m) if self.mouse.drag(self.inner, m) => {
self.move_to(self.row_at_drag((m.column, m.row))).into()
}
ct_event!(mouse down Left for column, row) => {
if self.inner.contains((*column, *row).into()) {
if let Some(new_row) = self.row_at_clicked((*column, *row)) {
self.move_to(new_row).into()
} else {
Outcome::Continue
}
} else {
Outcome::Continue
}
}
_ => Outcome::Continue,
}
);
let mut sas = ScrollAreaState::new()
.area(self.inner)
.v_scroll(&mut self.scroll);
let r = match sas.handle(event, MouseOnly) {
ScrollOutcome::Up(v) => {
if ListSelection::scroll_selected(&self.selection) {
self.move_up(1)
} else {
self.scroll_up(v)
}
}
ScrollOutcome::Down(v) => {
if ListSelection::scroll_selected(&self.selection) {
self.move_down(1)
} else {
self.scroll_down(v)
}
}
ScrollOutcome::VPos(v) => {
if ListSelection::scroll_selected(&self.selection) {
self.move_to(self.remap_offset_selection(v))
} else {
self.set_offset(v)
}
}
ScrollOutcome::Left(_) => false,
ScrollOutcome::Right(_) => false,
ScrollOutcome::HPos(_) => false,
ScrollOutcome::Continue => false,
ScrollOutcome::Unchanged => false,
ScrollOutcome::Changed => true,
};
if r {
return Outcome::Changed;
}
Outcome::Continue
}
}
pub type RowSetSelection = rat_ftable::selection::RowSetSelection;
impl ListSelection for RowSetSelection {
fn count(&self) -> usize {
<Self as TableSelection>::count(self)
}
fn is_selected(&self, n: usize) -> bool {
<Self as TableSelection>::is_selected_row(self, n)
}
fn lead_selection(&self) -> Option<usize> {
<Self as TableSelection>::lead_selection(self).map(|(_, y)| y)
}
fn validate_rows(&mut self, rows: usize) {
<Self as TableSelection>::validate_rows(self, rows);
}
fn items_added(&mut self, pos: usize, n: usize) {
<Self as TableSelection>::items_added(self, pos, n);
}
fn items_removed(&mut self, pos: usize, n: usize, rows: usize) {
<Self as TableSelection>::items_removed(self, pos, n, rows);
}
}
impl HandleEvent<Event, Regular, Outcome> for ListState<RowSetSelection> {
fn handle(&mut self, event: &Event, _: Regular) -> Outcome {
let res = if self.is_focused() {
match event {
ct_event!(keycode press Down) => self.move_down(1, false).into(),
ct_event!(keycode press SHIFT-Down) => self.move_down(1, true).into(),
ct_event!(keycode press Up) => self.move_up(1, false).into(),
ct_event!(keycode press SHIFT-Up) => self.move_up(1, true).into(),
ct_event!(keycode press CONTROL-Down) | ct_event!(keycode press End) => {
self.move_to(self.rows.saturating_sub(1), false).into()
}
ct_event!(keycode press SHIFT-End) => {
self.move_to(self.rows.saturating_sub(1), true).into()
}
ct_event!(keycode press CONTROL-Up) | ct_event!(keycode press Home) => {
self.move_to(0, false).into()
}
ct_event!(keycode press SHIFT-Home) => self.move_to(0, true).into(),
ct_event!(keycode press PageUp) => self
.move_up(max(1, self.page_len().saturating_sub(1)), false)
.into(),
ct_event!(keycode press SHIFT-PageUp) => self
.move_up(max(1, self.page_len().saturating_sub(1)), true)
.into(),
ct_event!(keycode press PageDown) => self
.move_down(max(1, self.page_len().saturating_sub(1)), false)
.into(),
ct_event!(keycode press SHIFT-PageDown) => self
.move_down(max(1, self.page_len().saturating_sub(1)), true)
.into(),
_ => Outcome::Continue,
}
} else {
Outcome::Continue
};
if res == Outcome::Continue {
self.handle(event, MouseOnly)
} else {
res
}
}
}
impl HandleEvent<Event, MouseOnly, Outcome> for ListState<RowSetSelection> {
fn handle(&mut self, event: &Event, _: MouseOnly) -> Outcome {
if self.is_obscured() {
return Outcome::Continue;
}
event_flow!(
return match event {
ct_event!(mouse any for m) | ct_event!(mouse any CONTROL for m)
if self.mouse.drag(self.inner, m)
|| self.mouse.drag2(self.inner, m, KeyModifiers::CONTROL) =>
{
self.move_to(self.row_at_drag((m.column, m.row)), true)
.into()
}
ct_event!(mouse down Left for column, row) => {
let pos = (*column, *row);
if self.inner.contains(pos.into()) {
if let Some(new_row) = self.row_at_clicked(pos) {
self.move_to(new_row, false).into()
} else {
Outcome::Continue
}
} else {
Outcome::Continue
}
}
ct_event!(mouse down ALT-Left for column, row) => {
let pos = (*column, *row);
if self.area.contains(pos.into()) {
if let Some(new_row) = self.row_at_clicked(pos) {
self.move_to(new_row, true).into()
} else {
Outcome::Continue
}
} else {
Outcome::Continue
}
}
ct_event!(mouse down CONTROL-Left for column, row) => {
let pos = (*column, *row);
if self.area.contains(pos.into()) {
if let Some(new_row) = self.row_at_clicked(pos) {
self.retire_selection();
if self.selection.is_selected_row(new_row) {
self.selection.remove(new_row);
} else {
self.move_to(new_row, true);
}
Outcome::Changed
} else {
Outcome::Continue
}
} else {
Outcome::Continue
}
}
_ => Outcome::Continue,
}
);
let mut sas = ScrollAreaState::new()
.area(self.inner)
.v_scroll(&mut self.scroll);
let r = match sas.handle(event, MouseOnly) {
ScrollOutcome::Up(v) => self.scroll_up(v),
ScrollOutcome::Down(v) => self.scroll_down(v),
ScrollOutcome::VPos(v) => self.set_offset(v),
ScrollOutcome::Left(_) => false,
ScrollOutcome::Right(_) => false,
ScrollOutcome::HPos(_) => false,
ScrollOutcome::Continue => false,
ScrollOutcome::Unchanged => false,
ScrollOutcome::Changed => true,
};
if r {
return Outcome::Changed;
}
Outcome::Continue
}
}
}
pub fn handle_events(state: &mut ListState, focus: bool, event: &Event) -> Outcome {
state.focus.set(focus);
HandleEvent::handle(state, event, Regular)
}
pub fn handle_mouse_events(state: &mut ListState, event: &Event) -> Outcome {
HandleEvent::handle(state, event, MouseOnly)
}