rat_menu/
menuitem.rs

1//!
2//! MenuItem for both MenuLine and PopupMenu.
3//!
4
5use crate::_private::NonExhaustive;
6use std::borrow::Cow;
7use std::ops::Range;
8use unicode_display_width::width;
9
10/// Separator style
11#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum Separator {
14    #[default]
15    Plain,
16    Empty,
17    Thick,
18    Double,
19    Dashed,
20    Dotted,
21}
22
23/// A menu item.
24#[derive(Debug, Clone)]
25pub struct MenuItem<'a> {
26    /// Menuitem text
27    pub item: Cow<'a, str>,
28    /// Text range to highlight. This is a byte-range into `item`.
29    pub highlight: Option<Range<usize>>,
30    /// Navigation key char.
31    pub navchar: Option<char>,
32    /// Right aligned text. To show the hotkey, or whatever.
33    /// Hotkey handling is not included in this crate.
34    pub right: Cow<'a, str>,
35    /// Disabled item.
36    pub disabled: bool,
37
38    /// Separator after the item.
39    pub separator: Option<Separator>,
40
41    pub non_exhaustive: NonExhaustive,
42}
43
44impl Default for MenuItem<'_> {
45    fn default() -> Self {
46        Self {
47            item: Default::default(),
48            highlight: None,
49            navchar: None,
50            right: Default::default(),
51            disabled: false,
52            separator: None,
53            non_exhaustive: NonExhaustive,
54        }
55    }
56}
57
58impl<'a> MenuItem<'a> {
59    pub fn new() -> Self {
60        Self {
61            item: Default::default(),
62            highlight: None,
63            navchar: None,
64            right: Default::default(),
65            disabled: false,
66            separator: Default::default(),
67            non_exhaustive: NonExhaustive,
68        }
69    }
70
71    /// Uses '_' as special character.
72    ///
73    /// __Item__
74    ///
75    /// The first '_' marks the navigation-char.
76    /// Pipe '|' separates the item text and the right text.
77    ///
78    /// __Separator__
79    ///
80    /// `\\` (underscore) is used as prefix and then
81    /// a fixed string to identify the separator:
82    ///
83    /// * `\\   ` - three blanks -> empty separator
84    /// * `\\___` - three underscores -> plain line
85    /// * `\\______` - six underscore -> thick line
86    /// * `\\===` - three equals -> double line
87    /// * `\\---` - three hyphen -> dashed line
88    /// * `\\...` - three dots -> dotted line
89    ///
90    pub fn new_parsed(s: &'a str) -> Self {
91        if is_separator_str(s) {
92            Self::new_sep(separator_str(s))
93        } else {
94            item_str(s)
95        }
96    }
97
98    /// New borrowed string as item text.
99    pub fn new_str(text: &'a str) -> Self {
100        Self {
101            item: Cow::Borrowed(text),
102            highlight: None,
103            navchar: None,
104            right: Cow::Borrowed(""),
105            disabled: false,
106            separator: Default::default(),
107            non_exhaustive: NonExhaustive,
108        }
109    }
110
111    /// New with owned string as item text.
112    pub fn new_string(text: String) -> Self {
113        Self {
114            item: Cow::Owned(text),
115            highlight: None,
116            navchar: None,
117            right: Default::default(),
118            disabled: false,
119            separator: Default::default(),
120            non_exhaustive: NonExhaustive,
121        }
122    }
123
124    /// New with navigation char and highlight.
125    /// Highlight here is a byte range into the text.
126    pub fn new_nav_str(text: &'a str, highlight: Range<usize>, navchar: char) -> Self {
127        Self {
128            item: Cow::Borrowed(text),
129            highlight: Some(highlight),
130            navchar: Some(navchar.to_ascii_lowercase()),
131            right: Cow::Borrowed(""),
132            disabled: false,
133            separator: Default::default(),
134            non_exhaustive: NonExhaustive,
135        }
136    }
137
138    /// New with navigation char and highlight.
139    /// Highlight here is a byte range into the text.
140    pub fn new_nav_string(text: String, highlight: Range<usize>, navchar: char) -> Self {
141        Self {
142            item: Cow::Owned(text),
143            highlight: Some(highlight),
144            navchar: Some(navchar.to_ascii_lowercase()),
145            right: Cow::Borrowed(""),
146            disabled: false,
147            separator: Default::default(),
148            non_exhaustive: NonExhaustive,
149        }
150    }
151
152    /// New separator.
153    ///
154    /// Such a menu item will be merged with the one before, unless
155    /// you set some item-text later.
156    pub fn new_sep(separator: Separator) -> Self {
157        Self {
158            item: Default::default(),
159            highlight: None,
160            navchar: None,
161            right: Default::default(),
162            disabled: false,
163            separator: Some(separator),
164            non_exhaustive: NonExhaustive,
165        }
166    }
167
168    /// Set the right text.
169    pub fn right(mut self, right: &'a str) -> Self {
170        self.right = Cow::Borrowed(right);
171        self
172    }
173
174    /// Set disabled.
175    pub fn disabled(mut self) -> Self {
176        self.disabled = true;
177        self
178    }
179
180    /// Adds a separator after the menuitem.
181    pub fn separator(mut self, separator: Separator) -> Self {
182        self.separator = Some(separator);
183        self
184    }
185
186    /// Text-width in graphemes for item.
187    pub fn item_width(&self) -> u16 {
188        width(self.item.as_ref()) as u16 - if self.navchar.is_some() { 1 } else { 0 }
189    }
190
191    /// Text-width in graphemes for right.
192    pub fn right_width(&self) -> u16 {
193        width(self.right.as_ref()) as u16
194    }
195
196    /// Text-height.
197    pub fn height(&self) -> u16 {
198        if self.separator.is_none() { 1 } else { 2 }
199    }
200}
201
202#[allow(clippy::needless_bool)]
203#[allow(clippy::if_same_then_else)]
204fn is_separator_str(s: &str) -> bool {
205    if s == "\\   " {
206        true
207    } else if s == "\\___" {
208        true
209    } else if s == "\\______" {
210        true
211    } else if s == "\\===" {
212        true
213    } else if s == "\\---" {
214        true
215    } else if s == "\\..." {
216        true
217    } else {
218        false
219    }
220}
221
222/// This uses `\\` (underscore) as prefix and
223/// a fixed string to identify the separator:
224///
225/// * `\\   ` - three blanks -> empty separator
226/// * `\\___` - three underscores -> plain line
227/// * `\\______` - six underscore -> thick line
228/// * `\\===` - three equals -> double line
229/// * `\\---` - three hyphen -> dashed line
230/// * `\\...` - three dots -> dotted line
231fn separator_str(s: &str) -> Separator {
232    if s == "\\   " {
233        Separator::Empty
234    } else if s == "\\___" {
235        Separator::Plain
236    } else if s == "\\______" {
237        Separator::Thick
238    } else if s == "\\===" {
239        Separator::Double
240    } else if s == "\\---" {
241        Separator::Dashed
242    } else if s == "\\..." {
243        Separator::Dotted
244    } else {
245        unreachable!()
246    }
247}
248
249/// Create a Line from the given text.
250/// The first '_' marks the navigation-char.
251/// Pipe '|' separates the item text and the right text.
252#[allow(clippy::collapsible_if)]
253fn item_str(txt: &str) -> MenuItem<'_> {
254    let mut idx_underscore = None;
255    let mut idx_navchar_start = None;
256    let mut idx_navchar_end = None;
257    let mut idx_pipe = None;
258    let cit = txt.char_indices();
259    for (idx, c) in cit {
260        if idx_underscore.is_none() && c == '_' {
261            idx_underscore = Some(idx);
262        } else if idx_underscore.is_some() && idx_navchar_start.is_none() {
263            idx_navchar_start = Some(idx);
264        } else if idx_navchar_start.is_some() && idx_navchar_end.is_none() {
265            idx_navchar_end = Some(idx);
266        }
267        if c == '|' {
268            idx_pipe = Some(idx);
269        }
270    }
271    if idx_navchar_start.is_some() && idx_navchar_end.is_none() {
272        idx_navchar_end = Some(txt.len());
273    }
274
275    if let Some(pipe) = idx_pipe {
276        if let Some(navchar_end) = idx_navchar_end {
277            if navchar_end > pipe {
278                idx_pipe = None;
279            }
280        }
281    }
282
283    let (text, right) = if let Some(idx_pipe) = idx_pipe {
284        (&txt[..idx_pipe], &txt[idx_pipe + 1..])
285    } else {
286        (txt, "")
287    };
288
289    if let Some(idx_navchar_start) = idx_navchar_start {
290        if let Some(idx_navchar_end) = idx_navchar_end {
291            MenuItem {
292                item: Cow::Borrowed(text),
293                highlight: Some(idx_navchar_start..idx_navchar_end),
294                navchar: Some(
295                    text[idx_navchar_start..idx_navchar_end]
296                        .chars()
297                        .next()
298                        .expect("char")
299                        .to_ascii_lowercase(),
300                ),
301                right: Cow::Borrowed(right),
302                ..Default::default()
303            }
304        } else {
305            unreachable!();
306        }
307    } else {
308        MenuItem {
309            item: Cow::Borrowed(text),
310            right: Cow::Borrowed(right),
311            ..Default::default()
312        }
313    }
314}