use crate::{
direction,
event::{AnyCb, Callback, Event, EventResult, Key},
rect::Rect,
view::{IntoBoxedView, Selector, View, ViewNotFound},
Cursive, Printer, Vec2, With,
};
use log::debug;
use std::rc::Rc;
use unicode_width::UnicodeWidthStr;
pub enum ListChild {
Row(String, Box<dyn View>),
Delimiter,
}
impl ListChild {
fn label(&self) -> &str {
match *self {
ListChild::Row(ref label, _) => label,
_ => "",
}
}
fn view(&mut self) -> Option<&mut dyn View> {
match *self {
ListChild::Row(_, ref mut view) => Some(view.as_mut()),
_ => None,
}
}
}
pub struct ListView {
children: Vec<ListChild>,
children_heights: Vec<usize>,
focus: usize,
on_select: Option<Rc<dyn Fn(&mut Cursive, &String)>>,
last_size: Vec2,
}
new_default!(ListView);
impl ListView {
pub fn new() -> Self {
ListView {
children: Vec::new(),
children_heights: Vec::new(),
focus: 0,
on_select: None,
last_size: Vec2::zero(),
}
}
pub fn len(&self) -> usize {
self.children.len()
}
pub fn is_empty(&self) -> bool {
self.children.is_empty()
}
pub fn children(&self) -> &[ListChild] {
&self.children[..]
}
pub fn get_row(&self, id: usize) -> &ListChild {
&self.children[id]
}
pub fn row_mut(&mut self, id: usize) -> &mut ListChild {
&mut self.children[id]
}
pub fn add_child<V: IntoBoxedView + 'static>(
&mut self,
label: &str,
view: V,
) {
let mut view = view.into_boxed_view();
view.take_focus(direction::Direction::none());
self.children.push(ListChild::Row(label.to_string(), view));
self.children_heights.push(0);
}
pub fn clear(&mut self) {
self.children.clear();
self.children_heights.clear();
self.focus = 0;
}
pub fn child<V: IntoBoxedView + 'static>(
self,
label: &str,
view: V,
) -> Self {
self.with(|s| s.add_child(label, view))
}
pub fn add_delimiter(&mut self) {
self.children.push(ListChild::Delimiter);
self.children_heights.push(0);
}
pub fn delimiter(self) -> Self {
self.with(Self::add_delimiter)
}
pub fn remove_child(&mut self, index: usize) -> ListChild {
self.children_heights.remove(index);
self.children.remove(index)
}
pub fn set_on_select<F>(&mut self, cb: F)
where
F: Fn(&mut Cursive, &String) + 'static,
{
self.on_select = Some(Rc::new(cb));
}
pub fn on_select<F>(self, cb: F) -> Self
where
F: Fn(&mut Cursive, &String) + 'static,
{
self.with(|s| s.set_on_select(cb))
}
pub fn focus(&self) -> usize {
self.focus
}
fn iter_mut<'a>(
&'a mut self,
from_focus: bool,
source: direction::Relative,
) -> Box<dyn Iterator<Item = (usize, &mut ListChild)> + 'a> {
match source {
direction::Relative::Front => {
let start = if from_focus { self.focus } else { 0 };
Box::new(self.children.iter_mut().enumerate().skip(start))
}
direction::Relative::Back => {
let end = if from_focus {
self.focus + 1
} else {
self.children.len()
};
Box::new(self.children[..end].iter_mut().enumerate().rev())
}
}
}
fn move_focus(
&mut self,
n: usize,
source: direction::Direction,
) -> EventResult {
let i = if let Some(i) = source
.relative(direction::Orientation::Vertical)
.and_then(|rel| {
self.iter_mut(true, rel)
.skip(1)
.filter_map(|p| try_focus(p, source))
.take(n)
.last()
}) {
i
} else {
return EventResult::Ignored;
};
self.focus = i;
EventResult::Consumed(self.on_select.clone().map(|cb| {
let i = self.focus();
let focused_string = String::from(self.children[i].label());
Callback::from_fn(move |s| cb(s, &focused_string))
}))
}
fn labels_width(&self) -> usize {
self.children
.iter()
.map(ListChild::label)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0)
}
fn check_focus_grab(&mut self, event: &Event) {
if let Event::Mouse {
offset,
position,
event,
} = *event
{
if !event.grabs_focus() {
return;
}
let mut position = match position.checked_sub(offset) {
None => return,
Some(pos) => pos,
};
for (i, (child, height)) in self
.children
.iter_mut()
.zip(&self.children_heights)
.enumerate()
{
if position.y < *height {
if let ListChild::Row(_, ref mut view) = child {
if view.take_focus(direction::Direction::none()) {
self.focus = i;
}
}
break;
} else {
position.y -= height;
}
}
}
}
}
fn try_focus(
(i, child): (usize, &mut ListChild),
source: direction::Direction,
) -> Option<usize> {
match *child {
ListChild::Delimiter => None,
ListChild::Row(_, ref mut view) => {
if view.take_focus(source) {
Some(i)
} else {
None
}
}
}
}
impl View for ListView {
fn draw(&self, printer: &Printer) {
if self.children.is_empty() {
return;
}
let offset = self.labels_width() + 1;
let mut y = 0;
debug!("Offset: {}", offset);
for (i, (child, &height)) in
self.children.iter().zip(&self.children_heights).enumerate()
{
match child {
ListChild::Row(ref label, ref view) => {
printer.print((0, y), label);
view.draw(
&printer
.offset((offset, y))
.cropped((printer.size.x, height))
.focused(i == self.focus),
);
}
ListChild::Delimiter => (),
}
y += height;
}
}
fn required_size(&mut self, req: Vec2) -> Vec2 {
let label_width = self
.children
.iter()
.map(ListChild::label)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
let view_size = direction::Orientation::Vertical.stack(
self.children.iter_mut().map(|c| match c {
ListChild::Delimiter => Vec2::new(0, 1),
ListChild::Row(_, ref mut view) => view.required_size(req),
}),
);
view_size + (1 + label_width, 0)
}
fn layout(&mut self, size: Vec2) {
self.last_size = size;
let label_width = self
.children
.iter()
.map(ListChild::label)
.map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
let spacing = 1;
let available = size.x.saturating_sub(label_width + spacing);
debug!("Available: {}", available);
self.children_heights.resize(self.children.len(), 0);
for (child, height) in self
.children
.iter_mut()
.filter_map(ListChild::view)
.zip(&mut self.children_heights)
{
*height = child.required_size(size).y;
child.layout(Vec2::new(available, *height));
}
}
fn on_event(&mut self, event: Event) -> EventResult {
if self.children.is_empty() {
return EventResult::Ignored;
}
self.check_focus_grab(&event);
let labels_width = self.labels_width();
if let ListChild::Row(_, ref mut view) = self.children[self.focus] {
let y = self.children_heights[..self.focus].iter().sum();
let offset = (labels_width + 1, y);
let result = view.on_event(event.relativized(offset));
if result.is_consumed() {
return result;
}
}
match event {
Event::Key(Key::Up) if self.focus > 0 => {
self.move_focus(1, direction::Direction::down())
}
Event::Key(Key::Down) if self.focus + 1 < self.children.len() => {
self.move_focus(1, direction::Direction::up())
}
Event::Key(Key::PageUp) => {
self.move_focus(10, direction::Direction::down())
}
Event::Key(Key::PageDown) => {
self.move_focus(10, direction::Direction::up())
}
Event::Key(Key::Home) | Event::Ctrl(Key::Home) => self
.move_focus(usize::max_value(), direction::Direction::back()),
Event::Key(Key::End) | Event::Ctrl(Key::End) => self
.move_focus(usize::max_value(), direction::Direction::front()),
Event::Key(Key::Tab) => {
self.move_focus(1, direction::Direction::front())
}
Event::Shift(Key::Tab) => {
self.move_focus(1, direction::Direction::back())
}
_ => EventResult::Ignored,
}
}
fn take_focus(&mut self, source: direction::Direction) -> bool {
let rel = source.relative(direction::Orientation::Vertical);
let i = if let Some(i) = self
.iter_mut(rel.is_none(), rel.unwrap_or(direction::Relative::Front))
.filter_map(|p| try_focus(p, source))
.next()
{
i
} else {
return false;
};
self.focus = i;
true
}
fn call_on_any<'a>(
&mut self,
selector: &Selector<'_>,
callback: AnyCb<'a>,
) {
for view in self.children.iter_mut().filter_map(ListChild::view) {
view.call_on_any(selector, callback);
}
}
fn focus_view(
&mut self,
selector: &Selector<'_>,
) -> Result<(), ViewNotFound> {
if let Some(i) = self
.children
.iter_mut()
.enumerate()
.filter_map(|(i, v)| v.view().map(|v| (i, v)))
.filter_map(|(i, v)| v.focus_view(selector).ok().map(|_| i))
.next()
{
self.focus = i;
Ok(())
} else {
Err(ViewNotFound)
}
}
fn important_area(&self, size: Vec2) -> Rect {
if self.children.is_empty() {
return Rect::from((0, 0));
}
let labels_width = self.labels_width();
let area = match self.children[self.focus] {
ListChild::Row(_, ref view) => {
let available =
Vec2::new(size.x.saturating_sub(labels_width + 1), 1);
view.important_area(available) + (labels_width, 0)
}
ListChild::Delimiter => Rect::from_size((0, 0), (size.x, 1)),
};
area + (0, self.focus)
}
}