1use crate::_private::NonExhaustive;
6use std::borrow::Cow;
7use std::ops::Range;
8use unicode_display_width::width;
9
10#[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#[derive(Debug, Clone)]
25pub struct MenuItem<'a> {
26 pub item: Cow<'a, str>,
28 pub highlight: Option<Range<usize>>,
30 pub navchar: Option<char>,
32 pub right: Cow<'a, str>,
35 pub disabled: bool,
37
38 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 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 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 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 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 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 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 pub fn right(mut self, right: &'a str) -> Self {
170 self.right = Cow::Borrowed(right);
171 self
172 }
173
174 pub fn disabled(mut self) -> Self {
176 self.disabled = true;
177 self
178 }
179
180 pub fn separator(mut self, separator: Separator) -> Self {
182 self.separator = Some(separator);
183 self
184 }
185
186 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 pub fn right_width(&self) -> u16 {
193 width(self.right.as_ref()) as u16
194 }
195
196 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
222fn 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#[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}