SelectView: make autojump opt-in

This commit is contained in:
Alexandre Bury 2018-09-27 16:01:37 -07:00
parent 333392e034
commit 3d81ad92ec
3 changed files with 90 additions and 22 deletions

View File

@ -12,7 +12,11 @@ use cursive::Cursive;
// one. // one.
fn main() { fn main() {
let mut select = SelectView::new().h_align(HAlign::Center); let mut select = SelectView::new()
// Center the text horizontally
.h_align(HAlign::Center)
// Use keyboard to jump to the pressed letters
.autojump();
// Read the list of cities from separate file, and fill the view with it. // Read the list of cities from separate file, and fill the view with it.
// (We include the file at compile-time to avoid runtime read errors.) // (We include the file at compile-time to avoid runtime read errors.)

View File

@ -404,6 +404,13 @@ where
ScrollStrategy::KeepRow => (), ScrollStrategy::KeepRow => (),
} }
} }
/// Returns the wrapped view.
pub fn into_inner(self) -> V {
self.inner
}
inner_getters!(self.inner: V);
} }
impl<V> View for ScrollView<V> impl<V> View for ScrollView<V>

View File

@ -47,18 +47,33 @@ use With;
/// ///
/// ``` /// ```
pub struct SelectView<T = String> { pub struct SelectView<T = String> {
// The core of the view: we store a list of items
// `Item` is more or less a `(String, Rc<T>)`.
items: Vec<Item<T>>, items: Vec<Item<T>>,
// When disabled, we cannot change selection.
enabled: bool, enabled: bool,
// the focus needs to be manipulable from callbacks
// Callbacks may need to manipulate focus, so give it some mutability.
focus: Rc<Cell<usize>>, focus: Rc<Cell<usize>>,
// This is a custom callback to include a &T. // This is a custom callback to include a &T.
// It will be called whenever "Enter" is pressed. // It will be called whenever "Enter" is pressed or when an item is clicked.
on_submit: Option<Rc<Fn(&mut Cursive, &T)>>, on_submit: Option<Rc<Fn(&mut Cursive, &T)>>,
// This callback is called when the selection is changed. // This callback is called when the selection is changed.
// TODO: add the previous selection? Indices?
on_select: Option<Rc<Fn(&mut Cursive, &T)>>, on_select: Option<Rc<Fn(&mut Cursive, &T)>>,
// If `true`, when a character is pressed, jump to the next item starting
// with this character.
autojump: bool,
align: Align, align: Align,
// `true` if we show a one-line view, with popup on selection. // `true` if we show a one-line view, with popup on selection.
popup: bool, popup: bool,
// We need the last offset to place the popup window // We need the last offset to place the popup window
// We "cache" it during the draw, so we need interior mutability. // We "cache" it during the draw, so we need interior mutability.
last_offset: Cell<Vec2>, last_offset: Cell<Vec2>,
@ -82,11 +97,30 @@ impl<T: 'static> SelectView<T> {
on_submit: None, on_submit: None,
align: Align::top_left(), align: Align::top_left(),
popup: false, popup: false,
autojump: false,
last_offset: Cell::new(Vec2::zero()), last_offset: Cell::new(Vec2::zero()),
last_size: Vec2::zero(), last_size: Vec2::zero(),
} }
} }
/// Sets the "auto-jump" property for this view.
///
/// If enabled, when a key is pressed, the selection will jump to the next
/// item beginning with the pressed letter.
pub fn set_autojump(&mut self, autojump: bool) {
self.autojump = autojump;
}
/// Sets the "auto-jump" property for this view.
///
/// If enabled, when a key is pressed, the selection will jump to the next
/// item beginning with the pressed letter.
///
/// Chainable variant.
pub fn autojump(self) -> Self {
self.with(|s| s.set_autojump(true))
}
/// Turns `self` into a popup select view. /// Turns `self` into a popup select view.
/// ///
/// Chainable variant. /// Chainable variant.
@ -148,6 +182,8 @@ impl<T: 'static> SelectView<T> {
/// Sets a callback to be used when `<Enter>` is pressed. /// Sets a callback to be used when `<Enter>` is pressed.
/// ///
/// Also happens if the user clicks an item.
///
/// The item currently selected will be given to the callback. /// The item currently selected will be given to the callback.
/// ///
/// Here, `V` can be `T` itself, or a type that can be borrowed from `T`. /// Here, `V` can be `T` itself, or a type that can be borrowed from `T`.
@ -163,6 +199,8 @@ impl<T: 'static> SelectView<T> {
/// Sets a callback to be used when `<Enter>` is pressed. /// Sets a callback to be used when `<Enter>` is pressed.
/// ///
/// Also happens if the user clicks an item.
///
/// The item currently selected will be given to the callback. /// The item currently selected will be given to the callback.
/// ///
/// Chainable variant. /// Chainable variant.
@ -249,6 +287,15 @@ impl<T: 'static> SelectView<T> {
} }
} }
/// Iterate on the items in this view.
///
/// Returns an iterator with each item and their labels.
pub fn iter(&self) -> impl Iterator<Item = (&str, &T)> {
self.items
.iter()
.map(|item| (item.label.as_str(), &*item.value))
}
/// Removes an item from the list. /// Removes an item from the list.
/// ///
/// Returns a callback in response to the selection change. /// Returns a callback in response to the selection change.
@ -415,6 +462,33 @@ impl<T: 'static> SelectView<T> {
) )
} }
fn on_char_event(&mut self, c: char) -> EventResult {
let i = {
// * Starting from the current focus, find the first item that
// match the char.
// * Cycle back to the beginning of the list when we reach the end.
// * This is achieved by chaining twice the iterator.
let iter = self.iter().chain(self.iter());
// We'll do a lowercase check.
let lower_c: Vec<char> = c.to_lowercase().collect();
let lower_c: &[char] = &lower_c;
if let Some((i, _)) = iter.enumerate().skip(self.focus() + 1).find(
|&(_, (label, _))| label.to_lowercase().starts_with(lower_c),
) {
i % self.len()
} else {
return EventResult::Ignored;
}
};
self.focus.set(i);
// Apply modulo in case we have a hit from the chained iterator
let cb = self.set_selection(i);
EventResult::Consumed(Some(cb))
}
fn on_event_regular(&mut self, event: Event) -> EventResult { fn on_event_regular(&mut self, event: Event) -> EventResult {
match event { match event {
Event::Key(Key::Up) if self.focus() > 0 => self.focus_up(1), Event::Key(Key::Up) if self.focus() > 0 => self.focus_up(1),
@ -456,25 +530,7 @@ impl<T: 'static> SelectView<T> {
Event::Key(Key::Enter) if self.on_submit.is_some() => { Event::Key(Key::Enter) if self.on_submit.is_some() => {
return self.submit(); return self.submit();
} }
Event::Char(c) => { Event::Char(c) if self.autojump => return self.on_char_event(c),
// Starting from the current focus,
// find the first item that match the char.
// Cycle back to the beginning of
// the list when we reach the end.
// This is achieved by chaining twice the iterator
let iter = self.items.iter().chain(self.items.iter());
if let Some((i, _)) = iter
.enumerate()
.skip(self.focus() + 1)
.find(|&(_, item)| item.label.starts_with(c))
{
// Apply modulo in case we have a hit
// from the chained iterator
self.focus.set(i % self.items.len());
} else {
return EventResult::Ignored;
}
}
_ => return EventResult::Ignored, _ => return EventResult::Ignored,
} }
@ -713,6 +769,7 @@ impl<T: 'static> View for SelectView<T> {
} }
} }
// We wrap each value in a `Rc` and add a label
struct Item<T> { struct Item<T> {
label: String, label: String,
value: Rc<T>, value: Rc<T>,