cursive_extras/views/advanced/
select_view.rs

1use std::{
2    cmp::Ordering,
3    fmt::Display,
4    sync::{
5        atomic::AtomicUsize,
6        Arc, RwLock,
7        atomic::Ordering as AtomicOrdering
8    }
9};
10use cursive_core::{
11    Cursive, impl_enabled, Printer,
12    Rect, Vec2, View, With,
13    direction::{Direction, Absolute},
14    view::{CannotFocus, Position},
15    event::{
16        Event, EventResult, Key,
17        MouseButton, MouseEvent
18    },
19    utils::markup::StyledString,
20    views::{
21        Dialog,
22        LayerPosition,
23        OnEventView,
24    }
25};
26use rust_utils::{chainable, encapsulated};
27use crate::{
28    c_focus, vlayout,
29    AdvancedButton
30};
31use super::ButtonContent;
32
33type SelectCallback<T> = Arc<dyn Fn(&mut Cursive, &T) + Send + Sync>;
34
35/// A view that is a lot like a `SelectView` but allows mutable access to the selection
36/// and allows multiline items
37//TODO: multiple selections
38#[derive(Clone)]
39#[encapsulated]
40pub struct AdvancedSelectView<T = String>
41    where T: 'static
42{
43    // the items
44    items: Vec<StyledItem<T>>,
45
46    // is this view enabled?
47    enabled: bool,
48
49    // the selected item id
50    selected: Arc<AtomicUsize>,
51
52    // autojump by keyboard key?
53    #[setter(doc = "Allow jumping to items starting with the the pressed letter key")]
54    #[chainable]
55    autojump: bool,
56
57    // show as a popup?
58    popup: bool,
59
60    // the offset for the popup view
61    popup_ofs: Arc<RwLock<Vec2>>,
62
63    // callback for selecting an item
64    selected_cb: SelectCallback<T>,
65
66    // callback for pressing Enter
67    submit_cb: SelectCallback<T>,
68
69    // the view's current size if it has already been calculated
70    size_cache: Option<Vec2>
71}
72
73impl AdvancedSelectView {
74    /// Add an item with the label as the value
75    #[chainable]
76    pub fn add_item_str<S: Display>(&mut self, label: S) {
77        self.add_item(label.to_string(), label.to_string());
78    }
79
80    /// Insert an item with the label as the value
81    pub fn insert_item_str<S: Display>(&mut self, index: usize, label: S) {
82        self.insert_item(index, label.to_string(), label.to_string());
83    }
84
85    /// Add all the strings from a string iterator
86    pub fn add_all_str<S, I>(&mut self, iter: I)
87        where
88            S: Display,
89            I: IntoIterator<Item = S>
90    {
91        for label in iter {
92            self.add_item_str(label);
93        }
94    }
95
96    /// Add all the strings from a string iterator
97    ///
98    /// Chainable version
99    #[must_use]
100    pub fn with_all_str<S, I>(mut self, iter: I) -> Self
101        where
102            S: Display,
103            I: IntoIterator<Item = S>
104    {
105        self.add_all_str(iter);
106        self
107    }
108}
109
110impl<T: Ord + 'static> AdvancedSelectView<T> {
111    /// Sort all the items by their ordering
112    pub fn sort(&mut self) { self.items.sort_by(|a, b| a.item.cmp(&b.item)); }
113}
114
115impl<T: Send + Sync + 'static> AdvancedSelectView<T> {
116    impl_enabled!(self.enabled);
117
118    /// Create a new `AdvancedSelectView`
119    #[must_use]
120    pub fn new() -> AdvancedSelectView<T> {
121        AdvancedSelectView {
122            items: vec![],
123            enabled: true,
124            selected: Arc::new(AtomicUsize::new(0)),
125            autojump: false,
126            popup: false,
127            popup_ofs: Arc::new(RwLock::new((0, 0).into())),
128            selected_cb: Arc::new(|_, _| { }),
129            submit_cb: Arc::new(|_, _| { }),
130            size_cache: None
131        }
132    }
133
134    /// Show this view as a pop up view
135    #[chainable]
136    pub fn set_popup(&mut self, popup: bool) {
137        self.popup = popup;
138        self.size_cache = None;
139    }
140
141    /// Set the select callback
142    #[chainable]
143    pub fn set_on_select<F: Fn(&mut Cursive, &T) + Send + Sync + 'static>(&mut self, cb: F) { self.selected_cb = Arc::new(cb); }
144
145    /// Set the callback when Enter is pressed
146    #[chainable]
147    pub fn set_on_submit<F: Fn(&mut Cursive, &T) + Send + Sync + 'static>(&mut self, cb: F) { self.submit_cb = Arc::new(cb); }
148
149    /// The current selection
150    ///
151    /// Returns `None` if the view is empty
152    pub fn selection(&self) -> Option<&T> {
153        Some(&self.items.get(self.selected_index())?.item)
154    }
155
156    /// The current selection as a mutable reference
157    ///
158    /// Returns `None` if the view is empty
159    pub fn selection_mut(&mut self) -> Option<&mut T> {
160        let index = self.selected_index();
161        Arc::get_mut(&mut self.items.get_mut(index)?.item)
162    }
163
164    /// The index of the current selection
165    pub fn selected_index(&self) -> usize { self.selected.load(AtomicOrdering::Relaxed) }
166
167    /// The label of the current selected item
168    pub fn selected_label(&self) -> Option<&StyledString> {
169        Some(self.items.get(self.selected_index())?.label.get_content())
170    }
171
172    /// The label of the current selected item
173    pub fn selected_label_mut(&mut self) -> Option<&mut StyledString> {
174        let index = self.selected_index();
175        Some(self.items.get_mut(index)?.label.get_content_mut())
176    }
177
178    /// Set the selected item index
179    pub fn set_selection(&mut self, index: usize) {
180        if index < self.items.len() { self.selected.store(index, AtomicOrdering::Relaxed); }
181    }
182
183    /// Set the selected item ID
184    ///
185    /// Chainable variant
186    #[must_use]
187    pub fn selected(mut self, index: usize) -> Self {
188        self.set_selection(index);
189        self
190    }
191
192    /// Move the selection down
193    ///
194    /// Returns true if successful
195    pub fn move_down(&mut self) -> bool {
196        let index = self.selected_index();
197        if index < self.items.len().saturating_sub(1) {
198            let new_selected = index + 1;
199            self.set_selection(new_selected);
200            return true;
201        }
202        false
203    }
204
205    /// Move the selection up
206    ///
207    /// Returns true if successful
208    pub fn move_up(&mut self) -> bool {
209        let index = self.selected_index();
210        if index != 0 {
211            let new_selected = index - 1;
212            self.set_selection(new_selected);
213            return true;
214        }
215        false
216    }
217
218    /// Clear all items
219    pub fn clear(&mut self) {
220        self.size_cache = None;
221        self.items.clear();
222        self.set_selection(0)
223    }
224
225    /// How many items are in this view?
226    pub fn len(&self) -> usize { self.items.len() }
227
228    /// Is this view empty?
229    pub fn is_empty(&self) -> bool { self.items.is_empty() }
230
231    /// Add an item to this view
232    #[chainable]
233    pub fn add_item<L: Into<StyledString>>(&mut self, label: L, item: T) {
234        let mut new_item = StyledItem::new(label, item);
235        if let Some(size) = self.size_cache {
236            new_item.fit_to_width(size.x, false);
237        }
238        self.items.push(new_item)
239    }
240
241    /// Add all items from an iterator
242    pub fn add_all<L, I>(&mut self, iter: I)
243        where
244            L: Into<StyledString>,
245            I: IntoIterator<Item = (L, T)>
246    {
247        for (label, item) in iter {
248            self.add_item(label, item);
249        }
250    }
251
252    /// Add all items from an iterator
253    ///
254    /// Chainable version
255    #[must_use]
256    pub fn with_all<L, I>(mut self, iter: I) -> Self
257        where
258            L: Into<StyledString>,
259            I: IntoIterator<Item = (L, T)>
260    {
261        self.add_all(iter);
262        self
263    }
264
265    /// Get an item at the index
266    pub fn get_item(&self, index: usize) -> Option<(&StyledString, &T)> {
267        let item = self.items.get(index)?;
268        Some((item.label.get_content(), &item.item))
269    }
270
271    /// Get an item (mutable reference) at the index
272    pub fn get_item_mut(&mut self, index: usize) -> Option<(&mut StyledString, &mut T)> {
273        let item = self.items.get_mut(index)?;
274        if let Some(t) = Arc::get_mut(&mut item.item) {
275            let label = &mut item.label;
276            Some((label.get_content_mut(), t))
277        }
278        else { None }
279    }
280
281    /// Iterate over all the items
282    pub fn iter(&self) -> impl Iterator<Item = (&StyledString, &T)> + DoubleEndedIterator + ExactSizeIterator {
283        self.items.iter().map(|item| (item.label.get_content(), &*item.item))
284    }
285
286    /// Iterate over all the items with mutability
287    pub fn iter_mut(&mut self) -> impl Iterator<Item = (&mut StyledString, &mut T)> + DoubleEndedIterator + ExactSizeIterator
288        where T: Clone
289    {
290        self.items.iter_mut().map(|item| (item.label.get_content_mut(), Arc::make_mut(&mut item.item)))
291    }
292
293    /// Remove an item at a specific index
294    pub fn remove_item(&mut self, index: usize) {
295        if index < self.items.len() {
296            self.items.remove(index);
297            if index >= self.len() {
298                self.set_selection(self.len() - 1);
299            }
300            let new_size = self.calc_size();
301            self.size_cache = Some(new_size);
302        }
303    }
304
305    /// Insert an item at a specific index
306    pub fn insert_item<L: Into<StyledString>>(&mut self, index: usize, label: L, item: T) {
307        if index < self.items.len() {
308            let mut new_item = StyledItem::new(label, item);
309            if let Some(size) = self.size_cache {
310                new_item.fit_to_width(size.x, false);
311            }
312            self.items.insert(index, new_item);
313        }
314    }
315
316    /// Sort the view by label
317    pub fn label_sort(&mut self) {
318        self.items
319            .sort_by(|a, b| a.label.get_content().source().cmp(b.label.get_content().source()));
320
321        self.calc_y_ofs();
322    }
323
324    /// Sort the view with the specified comparator function
325    pub fn sort_by<F>(&mut self, mut compare: F)
326        where
327            F: FnMut(&T, &T) -> Ordering,
328    {
329        self.items.sort_by(|a, b| compare(&a.item, &b.item));
330        self.calc_y_ofs();
331    }
332
333    /// Sort the view with a key extraction function
334    pub fn sort_by_key<K, F>(&mut self, mut key_of: F)
335        where
336            F: FnMut(&T) -> K,
337            K: Ord
338    {
339        self.items.sort_by_key(|item| key_of(&item.item));
340        self.calc_y_ofs();
341    }
342
343    /// Move an item up in the view
344    pub fn move_item_up(&mut self, index: usize) {
345        if index > 0 {
346            self.items.swap(index, index - 1);
347            self.calc_y_ofs();
348        }
349    }
350
351    /// Move an item down in the view
352    pub fn move_item_down(&mut self, index: usize) {
353        if index < self.items.len() - 1 {
354            self.items.swap(index, index + 1);
355            self.calc_y_ofs();
356        }
357    }
358
359    fn open_popup(&mut self) -> EventResult {
360        let index = self.selected_index();
361        let mut sv_layout = vlayout!();
362
363        for (i, item) in self.items.iter().enumerate() {
364            let selected = self.selected.clone();
365            let submit_cb = self.submit_cb.clone();
366            let selected_cb = self.selected_cb.clone();
367            let data = item.item.clone();
368            sv_layout.add_child(
369                AdvancedButton::new_with_data(
370                    item.label.get_content().clone(),
371                    item.item.clone(),
372                    move |root| {
373                        selected.store(i, AtomicOrdering::Relaxed);
374                        root.pop_layer();
375                        submit_cb(root, &data);
376                        selected_cb(root, &data);
377                    }
378                )
379            );
380            if i == index { sv_layout.set_focus_index(i).unwrap(); }
381        }
382
383        let y_ofs = self.items[index].y_ofs;
384        let ofs = *(self.popup_ofs.read().unwrap());
385        let ofs = ofs.saturating_sub((0, y_ofs + 1)) + (1, 0);
386
387        EventResult::with_cb_once(move |root| {
388            let current_offset = root
389                .screen()
390                .layer_offset(LayerPosition::FromFront(0))
391                .unwrap_or_else(Vec2::zero);
392
393            let offset = ofs.signed() - current_offset;
394            root.screen_mut().add_layer_at(
395                Position::parent(offset),
396                c_focus!(
397                    Dialog::around(sv_layout)
398                        .wrap_with(OnEventView::new)
399                        .on_event(Event::Key(Key::Esc), |r| { r.pop_layer(); })
400                )
401            );
402        })
403    }
404
405    // the height of this view
406    fn view_height(&self) -> usize { self.items.iter().map(|item| item.size.y).sum() }
407
408    // recalculate the y offset of all the items
409    fn calc_y_ofs(&mut self) {
410        let mut y = 0;
411        for item in &mut self.items {
412            item.y_ofs = y;
413            y += item.size.y;
414        }
415    }
416
417    fn calc_size(&mut self) -> Vec2 {
418        let width = self
419            .items
420            .iter()
421            .map(|item| item.size.x)
422            .max()
423            .unwrap_or(1);
424
425        (width, self.view_height()).into()
426    }
427}
428
429impl<T: Send + Sync + 'static> View for AdvancedSelectView<T> {
430    fn draw(&self, printer: &Printer) {
431        let index = self.selected_index();
432        if printer.size.x == 0 || printer.size.y == 0 { return; }
433        if self.popup && !self.items.is_empty() {
434            *(self.popup_ofs.write().unwrap()) = printer.offset;
435            self.items[index].draw(printer, self.enabled, printer.focused, true);
436        }
437        else {
438            for (i, item) in self.items.iter().enumerate() {
439                item.draw(printer, self.enabled, i == index && printer.focused, false);
440            }
441        }
442    }
443
444    fn required_size(&mut self, bound: Vec2) -> Vec2 {
445        let index = self.selected_index();
446        if bound.x == 0 || bound.y == 0 { return (0, 0).into() }
447        let new_size = if self.popup && !self.items.is_empty() {
448            let item = &mut self.items[index];
449            item.fit_to_width(bound.x, true);
450            item.label.size(true)
451        }
452        else {
453            if let Some(size) = self.size_cache {
454                if let Some(item) = self.items.get(0) {
455                    if item.size.x > 0 { return size; }
456                }
457            }
458            for item in &mut self.items {
459                item.fit_to_width(bound.x, false);
460            }
461
462            self.calc_size()
463        };
464        new_size
465    }
466
467    fn layout(&mut self, size: Vec2) {
468        let index = self.selected_index();
469        if size.x == 0 || size.y == 0 { return; }
470        self.calc_y_ofs();
471        for item in &mut self.items {
472            item.fit_to_width(size.x, false);
473        }
474
475        if self.popup {
476            let selected = &mut self.items[index];
477            selected.fit_to_width(size.x, true);
478        }
479        self.size_cache = Some(size);
480    }
481
482    fn take_focus(&mut self, dir: Direction) -> Result<EventResult, CannotFocus> {
483        if self.enabled && !self.items.is_empty() {
484            if !self.popup {
485                match dir {
486                    Direction::Abs(Absolute::Up) => self.set_selection(0),
487                    Direction::Abs(Absolute::Down) => self.set_selection(self.items.len().saturating_sub(1)),
488                    _ => { }
489                }
490            }
491            Ok(EventResult::consumed())
492        }
493        else { Err(CannotFocus) }
494    }
495
496    fn on_event(&mut self, event: Event) -> EventResult {
497        let index = self.selected_index();
498        if self.enabled {
499            match event {
500                Event::Key(Key::Up) if !self.popup => if !self.move_up() { return EventResult::Ignored; }
501                Event::Key(Key::Down) if !self.popup => if !self.move_down() { return EventResult::Ignored; },
502                Event::Key(Key::End) if !self.popup => self.set_selection(self.items.len() - 1),
503                Event::Key(Key::Home) if !self.popup => self.set_selection(0),
504
505                Event::Key(Key::PageUp) if !self.popup => {
506                    if index < 10 {
507                        self.set_selection(0);
508                    }
509                    else {
510                        self.set_selection(index - 10);
511                    }
512                }
513
514                Event::Key(Key::PageDown) if !self.popup => {
515                    if self.items.len() < 10 {
516                        self.set_selection(self.items.len() - 1);
517                    }
518                    else if index > self.items.len() - 10 {
519                        self.set_selection(self.items.len() - 1);
520                    }
521                    else {
522                        self.set_selection(index + 10);
523                    }
524                }
525
526                Event::Key(Key::Enter) => {
527                    if self.is_empty() { return EventResult::Ignored; }
528                    if self.popup { return self.open_popup(); }
529                    let callback = self.submit_cb.clone();
530                    let item = self.items[index].item.clone();
531                    return EventResult::with_cb_once(move |root| callback(root, &item))
532                }
533
534                Event::Mouse {
535                    event: MouseEvent::Press(button),
536                    position,
537                    offset
538                }
539                if position.checked_sub(offset).is_some() && !self.popup => {
540                    for (i, item) in self.items.iter().enumerate() {
541                        if item.has_mouse_pos(position - offset) {
542                            self.selected.store(i, AtomicOrdering::Relaxed);
543                        }
544                    }
545
546                    if button == MouseButton::Left {
547                        let callback = self.submit_cb.clone();
548                        let item = self.items[self.selected_index()].item.clone();
549                        return EventResult::with_cb_once(move |root| callback(root, &item))
550                    }
551                }
552
553                Event::Mouse {
554                    event: MouseEvent::Release(MouseButton::Left),
555                    position,
556                    offset
557                }
558                if self.popup => {
559                    let item = &self.items[index];
560                    let b_rect = Rect::from_size((0, 0), item.size);
561
562                    if let Some(new_pos) = position.checked_sub(offset) {
563                        if b_rect.contains(new_pos) {
564                            return self.open_popup();
565                        }
566                    }
567                }
568
569                Event::Char(c) if self.autojump && !self.popup => {
570                    let lc = c.to_ascii_lowercase();
571                    for (i, item) in self.items.iter().enumerate() {
572                        let raw_label = item.label.get_content().source();
573                        if raw_label.is_empty() || i <= index { continue; }
574                        else {
575                            let first_char = raw_label
576                                .chars()
577                                .next().unwrap()
578                                .to_ascii_lowercase();
579
580                            if lc == first_char {
581                                self.set_selection(i);
582                                break;
583                            }
584                        }
585                    }
586                }
587
588                Event::WindowResize => {
589                    self.size_cache = None;
590
591                    for item in &mut self.items {
592                        item.size = (0, 1).into();
593                    }
594
595                    return EventResult::Ignored;
596                }
597
598                _ => return EventResult::Ignored
599            }
600
601            if index >= self.items.len() {
602                self.set_selection(self.items.len() - 1);
603            }
604
605            if self.items.is_empty() { EventResult::Ignored }
606            else {
607                let callback = self.selected_cb.clone();
608                let item = self.items[self.selected_index()].item.clone();
609                EventResult::with_cb_once(move |root| callback(root, &item))
610            }
611        }
612        else { EventResult::Ignored }
613    }
614
615    fn important_area(&self, size: Vec2) -> Rect {
616        if self.popup { Rect::from_size((0, 0), size) }
617        else {
618            let loc = (0, self.items[self.selected_index()].y_ofs);
619            Rect::from_size(loc, self.items[self.selected_index()].size)
620        }
621    }
622}
623
624impl<T: Send + Sync + 'static> Default for AdvancedSelectView<T> {
625    fn default() -> Self { Self::new() }
626}
627
628#[derive(Clone)]
629struct StyledItem<T: 'static> {
630    // this item's label
631    label: ButtonContent,
632
633    // the value
634    item: Arc<T>,
635
636    // size
637    size: Vec2,
638
639    // Y offset
640    y_ofs: usize
641}
642
643impl<T: 'static> StyledItem<T> {
644    fn new<L: Into<StyledString>>(label: L, item: T) -> StyledItem<T> {
645        StyledItem {
646            label: ButtonContent::new(label),
647            item: Arc::new(item),
648            size: (0, 1).into(),
649            y_ofs: 0
650        }
651    }
652
653    fn has_mouse_pos(&self, pos: Vec2) -> bool {
654        Rect::from_size((0, self.y_ofs), self.size)
655            .contains(pos)
656    }
657
658    fn draw(&self, printer: &Printer, enabled: bool, focused: bool, popup: bool) {
659        self.label.draw(printer, if popup { (0, 0).into() } else { (0, self.y_ofs).into() }, enabled, focused, popup);
660    }
661
662    fn fit_to_width(&mut self, new_width: usize, popup: bool) {
663        self.label.fit_to_width(new_width);
664        self.size = self.label.size(popup);
665    }
666}