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 { self.selected.store(0, AtomicOrdering::Relaxed) }
507                    else {
508                        let new_selected = index - 10;
509                        self.selected.store(new_selected, AtomicOrdering::Relaxed);
510                    }
511                }
512
513                Event::Key(Key::PageDown) if !self.popup => {
514                    if self.items.len() < 10 { self.selected.store(self.items.len() - 1, AtomicOrdering::Relaxed) }
515                    else if index > self.items.len() - 10 { self.selected.store(self.items.len() - 1, AtomicOrdering::Relaxed); }
516                    else {
517                        let new_selected = index + 10;
518                        self.selected.store(new_selected, AtomicOrdering::Relaxed);
519                    }
520                }
521
522                Event::Key(Key::Enter) => {
523                    if self.is_empty() { return EventResult::Ignored; }
524                    if self.popup { return self.open_popup(); }
525                    let callback = self.submit_cb.clone();
526                    let item = self.items[index].item.clone();
527                    return EventResult::with_cb_once(move |root| callback(root, &item))
528                }
529
530                Event::Mouse {
531                    event: MouseEvent::Press(button),
532                    position,
533                    offset
534                }
535                if position.checked_sub(offset).is_some() && !self.popup => {
536                    for (i, item) in self.items.iter().enumerate() {
537                        if item.has_mouse_pos(position - offset) {
538                            self.selected.store(i, AtomicOrdering::Relaxed);
539                        }
540                    }
541
542                    if button == MouseButton::Left {
543                        let callback = self.submit_cb.clone();
544                        let item = self.items[index].item.clone();
545                        return EventResult::with_cb_once(move |root| callback(root, &item))
546                    }
547                }
548
549                Event::Mouse {
550                    event: MouseEvent::Release(MouseButton::Left),
551                    position,
552                    offset
553                } if self.popup => {
554                    let item = &self.items[index];
555                    let b_rect = Rect::from_size((0, 0), item.size);
556
557                    if let Some(new_pos) = position.checked_sub(offset) {
558                        if b_rect.contains(new_pos) {
559                            return self.open_popup();
560                        }
561                    }
562                }
563
564                Event::Char(c) if self.autojump && !self.popup => {
565                    let lc = c.to_ascii_lowercase();
566                    for (i, item) in self.items.iter().enumerate() {
567                        let raw_label = item.label.get_content().source();
568                        if raw_label.is_empty() || i <= index { continue; }
569                        else {
570                            let first_char = raw_label
571                                .chars()
572                                .next().unwrap()
573                                .to_ascii_lowercase();
574                            if lc == first_char {
575                                self.set_selection(i);
576                                break;
577                            }
578                        }
579                    }
580                }
581
582                Event::WindowResize => {
583                    self.size_cache = None;
584                    for item in &mut self.items {
585                        item.size = (0, 1).into();
586                    }
587                    return EventResult::Ignored;
588                }
589
590                _ => return EventResult::Ignored
591            }
592
593            if index >= self.items.len() {
594                self.set_selection(self.items.len() - 1);
595            }
596
597            if self.items.is_empty() { EventResult::Ignored }
598            else {
599                let callback = self.selected_cb.clone();
600                let item = self.items[index].item.clone();
601                EventResult::with_cb_once(move |root| callback(root, &item))
602            }
603        }
604        else { EventResult::Ignored }
605    }
606
607    fn important_area(&self, size: Vec2) -> Rect {
608        if self.popup { Rect::from_size((0, 0), size) }
609        else {
610            let loc = (0, self.items[self.selected_index()].y_ofs);
611            Rect::from_size(loc, self.items[self.selected_index()].size)
612        }
613    }
614}
615
616impl<T: Send + Sync + 'static> Default for AdvancedSelectView<T> {
617    fn default() -> Self { Self::new() }
618}
619
620#[derive(Clone)]
621struct StyledItem<T: 'static> {
622    // this item's label
623    label: ButtonContent,
624
625    // the value
626    item: Arc<T>,
627
628    // size
629    size: Vec2,
630
631    // Y offset
632    y_ofs: usize
633}
634
635impl<T: 'static> StyledItem<T> {
636    fn new<L: Into<StyledString>>(label: L, item: T) -> StyledItem<T> {
637        StyledItem {
638            label: ButtonContent::new(label),
639            item: Arc::new(item),
640            size: (0, 1).into(),
641            y_ofs: 0
642        }
643    }
644
645    fn has_mouse_pos(&self, pos: Vec2) -> bool {
646        Rect::from_size((0, self.y_ofs), self.size)
647            .contains(pos)
648    }
649
650    fn draw(&self, printer: &Printer, enabled: bool, focused: bool, popup: bool) {
651        self.label.draw(printer, if popup { (0, 0).into() } else { (0, self.y_ofs).into() }, enabled, focused, popup);
652    }
653
654    fn fit_to_width(&mut self, new_width: usize, popup: bool) {
655        self.label.fit_to_width(new_width);
656        self.size = self.label.size(popup);
657    }
658}