1use crate::_private::NonExhaustive;
6use std::borrow::Cow;
7use std::ops::Range;
8use unicode_segmentation::UnicodeSegmentation;
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 self.item.graphemes(true).count() as u16 - if self.navchar.is_some() { 1 } else { 0 }
189 }
190
191 pub fn right_width(&self) -> u16 {
193 self.right.graphemes(true).count() as u16
194 }
195
196 pub fn height(&self) -> u16 {
198 if self.separator.is_none() {
199 1
200 } else {
201 2
202 }
203 }
204}
205
206#[allow(clippy::needless_bool)]
207#[allow(clippy::if_same_then_else)]
208fn is_separator_str(s: &str) -> bool {
209 if s == "\\ " {
210 true
211 } else if s == "\\___" {
212 true
213 } else if s == "\\______" {
214 true
215 } else if s == "\\===" {
216 true
217 } else if s == "\\---" {
218 true
219 } else if s == "\\..." {
220 true
221 } else {
222 false
223 }
224}
225
226fn separator_str(s: &str) -> Separator {
236 if s == "\\ " {
237 Separator::Empty
238 } else if s == "\\___" {
239 Separator::Plain
240 } else if s == "\\______" {
241 Separator::Thick
242 } else if s == "\\===" {
243 Separator::Double
244 } else if s == "\\---" {
245 Separator::Dashed
246 } else if s == "\\..." {
247 Separator::Dotted
248 } else {
249 unreachable!()
250 }
251}
252
253fn item_str(txt: &str) -> MenuItem<'_> {
257 let mut idx_underscore = None;
258 let mut idx_navchar_start = None;
259 let mut idx_navchar_end = None;
260 let mut idx_pipe = None;
261 let cit = txt.char_indices();
262 for (idx, c) in cit {
263 if idx_underscore.is_none() && c == '_' {
264 idx_underscore = Some(idx);
265 } else if idx_underscore.is_some() && idx_navchar_start.is_none() {
266 idx_navchar_start = Some(idx);
267 } else if idx_navchar_start.is_some() && idx_navchar_end.is_none() {
268 idx_navchar_end = Some(idx);
269 }
270 if c == '|' {
271 idx_pipe = Some(idx);
272 }
273 }
274 if idx_navchar_start.is_some() && idx_navchar_end.is_none() {
275 idx_navchar_end = Some(txt.len());
276 }
277
278 if let Some(pipe) = idx_pipe {
279 if let Some(navchar_end) = idx_navchar_end {
280 if navchar_end > pipe {
281 idx_pipe = None;
282 }
283 }
284 }
285
286 let (text, right) = if let Some(idx_pipe) = idx_pipe {
287 (&txt[..idx_pipe], &txt[idx_pipe + 1..])
288 } else {
289 (txt, "")
290 };
291
292 if let Some(idx_navchar_start) = idx_navchar_start {
293 if let Some(idx_navchar_end) = idx_navchar_end {
294 MenuItem {
295 item: Cow::Borrowed(text),
296 highlight: Some(idx_navchar_start..idx_navchar_end),
297 navchar: Some(
298 text[idx_navchar_start..idx_navchar_end]
299 .chars()
300 .next()
301 .expect("char")
302 .to_ascii_lowercase(),
303 ),
304 right: Cow::Borrowed(right),
305 ..Default::default()
306 }
307 } else {
308 unreachable!();
309 }
310 } else {
311 MenuItem {
312 item: Cow::Borrowed(text),
313 right: Cow::Borrowed(right),
314 ..Default::default()
315 }
316 }
317}