cartographer_rs/
menu.rs

1mod interact;
2
3#[cfg_attr(
4    feature = "serde_serialize",
5    derive(serde::Serialize, serde::Deserialize)
6)]
7#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8/// A data structure representing a line-item in a menu
9///
10/// The recommended way of constructing these is to use  the [`menu_item!`](crate::menu_item!) macro
11/// though the output will be the same
12///
13/// ## Example
14/// ```
15/// let menu_item = MenuItem::new("A Menu Item".to_string())
16///     .visible_at_rest(true)
17///     .at_rest_position(1);
18///
19/// // is the same as
20///
21/// menu_item!("A Menu Item", true, 1);
22/// ```
23pub struct MenuItem {
24    /// The String that will display for this item in the menu
25    visible_name: String,
26
27    /// Toggles if this item will be shown when no search terms are available
28    visible_at_rest: bool,
29
30    /// Optional feature that will let you specify in what order the MenuItems will be
31    /// displayed
32    at_rest_position: Option<usize>,
33
34    /// A list of strings that will also be used, in addition to the `visible_name`,
35    /// when processing the search results
36    alternative_matches: Option<Vec<String>>,
37}
38
39impl MenuItem {
40    /// Create a new MenuItem with the visible name specified
41    pub fn new(visible_name: String) -> Self {
42        MenuItem {
43            visible_name,
44            visible_at_rest: true,
45            at_rest_position: None,
46            alternative_matches: None,
47        }
48    }
49
50    /// Set whether a [`MenuItem`] is visible when no search is showing
51    pub fn visible_at_rest(self, visible: bool) -> Self {
52        MenuItem {
53            visible_at_rest: visible,
54            ..self
55        }
56    }
57
58    /// Set a [`MenuItem`]'s resting position in the no search menu
59    /// Note: Won't have any effect if visible_at_rest is false
60    pub fn at_rest_position(self, position: usize) -> Self {
61        MenuItem {
62            at_rest_position: Some(position),
63            ..self
64        }
65    }
66
67    /// Set alternative matches for a [`MenuItem`]. These are strings that this item will
68    /// match to when searching - in addition to the visible_name
69    pub fn add_alternative_match(self, new_matches: Vec<String>) -> Self {
70        let mut cur_matches: Vec<String>;
71        if self.alternative_matches.is_none() {
72            cur_matches = Vec::new();
73        } else {
74            cur_matches = self.alternative_matches.unwrap();
75        }
76        for i in new_matches {
77            cur_matches.push(i);
78        }
79        MenuItem {
80            alternative_matches: Some(cur_matches),
81            ..self
82        }
83    }
84}
85
86/// The Menu struct that contains the information and
87/// functions for displaying the menus
88#[derive(Clone, Debug, PartialEq)]
89pub struct Menu {
90    /// The text to be displayed on the same line as user input will be shown.
91    /// To make it extra clear, try adding a semicolon and a space. (e.g. `prompt: "Pick and item: "`)
92    prompt: String,
93
94    /// The Vector of [`MenuItem`]s
95    items: Vec<MenuItem>,
96
97    /// The [`MenuOptions`] to use when displaying the menu
98    configuration: MenuOptions,
99}
100
101impl Menu {
102    /// Create a new Menu from a prompt, list of [`MenuItem`](crate::MenuItem)s, and an optional
103    /// [`MenuOptions`](crate::MenuOptions) instance. If configuration is `None`, then the default
104    /// is used
105    pub fn new(
106        prompt: String,
107        menu_items: Vec<MenuItem>,
108        configuration: Option<MenuOptions>,
109    ) -> Menu {
110        Menu {
111            prompt,
112            items: menu_items,
113            configuration: {
114                if let Some(configuration) = configuration {
115                    configuration
116                } else {
117                    MenuOptions {
118                        ..MenuOptions::default()
119                    }
120                }
121            },
122        }
123    }
124}
125
126/// Controls and characters that can be configured
127/// to change the way the menu acts and displays
128///
129///
130/// ## Example
131/// ```
132/// let options = MenuOptions::new()
133///     .cursor("→")
134///     .select_key(console::Key::Tab)
135///     .max_lines_visible(6);
136///
137/// menu!("Only 6 lines are visible!",
138///     menu_items_list,
139///     options
140///     )
141///
142/// ```
143///
144#[derive(Clone, Debug, PartialEq)]
145pub struct MenuOptions {
146    /// The user's cursor while they navigate
147    cursor: String,
148    cursor_width: usize,
149
150    /// The char used to indicate an item is selected
151    selected_indicator: String,
152    selected_indicator_width: usize,
153
154    /// The button the user uses to select an item
155    select_key: console::Key,
156
157    /// The maximum number of vertical lines the menu can have
158    max_lines_visible: usize,
159
160    ///  The minimum search score for an item to be displayed in the menu
161    ///  The lower the number, the more results will be displayed
162    min_search_threshold: f32,
163
164    /// Configures if selected items stay visible in search results
165    show_select_in_search: bool,
166
167    /// Set if the menu returns the first selected item
168    only_one: bool,
169
170    /// Set if the menu cleans up the terminal after exiting
171    clear_menu_on_exit: bool,
172}
173
174impl MenuOptions {
175    /// Create a new [`MenuOptions`] with all the defaults
176    pub fn new() -> Self {
177        MenuOptions {
178            ..MenuOptions::default()
179        }
180    }
181
182    /// Set the user's row-indicator/cursor to a custom character.
183    /// The default is: '>'
184    pub fn cursor(self, cursor: &str) -> Self {
185        MenuOptions { cursor: cursor.to_string(), ..self }
186    }
187    /// Override the space given for the cursor navigation column
188    pub fn cursor_width(self, cursor_width: usize) -> Self {
189        MenuOptions { cursor_width, ..self }
190    }
191
192    /// Set the "Item Selected" indicator to a custom character.
193    /// Defaults is: 'X'
194    pub fn selected_indicator(self, indicator: &str) -> Self {
195        MenuOptions {
196            selected_indicator: indicator.to_string(),
197            ..self
198        }
199    }
200    /// Override the space given for the cursor navigation column
201    pub fn selected_indicator_width(self, indicator_width: usize) -> Self {
202        MenuOptions { selected_indicator_width: indicator_width, ..self }
203    }
204    /// Set the key that is used to select an item.
205    /// The default is: [`console::Key::Char(' ')`]
206    pub fn select_key(self, key: console::Key) -> Self {
207        MenuOptions {
208            select_key: key,
209            ..self
210        }
211    }
212    /// Set the maximum number of items that will be displayed at any one time.
213    /// The default is: 10
214    pub fn max_lines_visible(self, max_lines: usize) -> Self {
215        MenuOptions {
216            max_lines_visible: max_lines,
217            ..self
218        }
219    }
220    /// Set the degree of "fuzziness" that it will match too. Higher numbers will return more
221    /// results, but less accurate ones. Has to be 1.0 >= x >= 0 or will panic
222    /// The default is: 0.005
223    pub fn minimum_search_threshold(self, threshold: f32) -> Self {
224        assert!( (0.0..1.0).contains(&threshold) );
225        MenuOptions {
226            min_search_threshold: threshold,
227            ..self
228        }
229    }
230    /// Set if 'selected' rows are still shown during searches they aren't results for
231    /// The default is: true
232    pub fn show_selected_in_search(self, show_in_search: bool) -> Self {
233        MenuOptions {
234            show_select_in_search: show_in_search,
235            ..self
236        }
237    }
238    /// Set if the menu should exit and return only the first user selection.
239    /// The default is: false
240    pub fn only_one_selection(self, only_one: bool) -> Self {
241        MenuOptions { only_one, ..self }
242    }
243    /// Set if the menu should delete any left-over lines from the terminal.
244    /// The default is: true
245    pub fn clear_on_close(self, do_clear: bool) -> Self {
246        MenuOptions {
247            clear_menu_on_exit: do_clear,
248            ..self
249        }
250    }
251}
252
253impl Default for MenuOptions {
254    fn default() -> Self {
255        MenuOptions {
256            cursor: ">".to_string(),
257            cursor_width: 1,
258            selected_indicator: "X".to_string(),
259            selected_indicator_width: 1,
260            select_key: console::Key::Char(' '),
261            max_lines_visible: 10,
262            min_search_threshold: 0.005,
263            show_select_in_search: true,
264            only_one: false,
265            clear_menu_on_exit: true,
266        }
267    }
268}