1use ratatui::{buffer::Buffer, layout::Rect, style::Style};
2use unicode_segmentation::UnicodeSegmentation;
3
4use crate::label::{Label, Span};
5use crate::text::cells;
6
7const RULE: &str = "─";
8const ELLIPSIS: &str = "…";
9const SIDE_MIN: u16 = 6;
10const GROUP_GAP: u16 = 2;
11const SECTION_GAP: u16 = 0;
12const TAIL_GAP: u16 = 2;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum Row {
17 Title,
18 Groups,
19 Caption,
20 Sections,
21 Facts,
22 Crumbs,
23 Blank,
24}
25
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Numbers {
29 #[default]
30 Across,
31 InGroup,
32 Off,
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36#[non_exhaustive]
37pub struct Head {
38 pub rows: Vec<Row>,
39 pub numbers: Numbers,
40 pub lone: bool,
41}
42
43impl Head {
44 pub fn new(rows: impl IntoIterator<Item = Row>) -> Self {
45 Head {
46 rows: rows.into_iter().collect(),
47 numbers: Numbers::Across,
48 lone: false,
49 }
50 }
51
52 pub fn numbers(mut self, numbers: Numbers) -> Self {
53 self.numbers = numbers;
54 self
55 }
56
57 pub fn lone(mut self, lone: bool) -> Self {
58 self.lone = lone;
59 self
60 }
61}
62
63impl Default for Head {
64 fn default() -> Self {
65 Head::new([
66 Row::Title,
67 Row::Groups,
68 Row::Sections,
69 Row::Facts,
70 Row::Crumbs,
71 ])
72 }
73}
74
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum Focus {
78 #[default]
79 Content,
80 Groups,
81 Sections,
82 Slot,
83}
84
85fn width(text: &str) -> u16 {
86 u16::try_from(cells(text)).unwrap_or(u16::MAX)
87}
88
89fn spans_width<T: AsRef<str>>(spans: &[(T, Style)]) -> u16 {
90 spans.iter().fold(0u16, |total, (text, _)| {
91 total.saturating_add(width(text.as_ref()))
92 })
93}
94
95fn digits(number: usize) -> String {
96 let shown = if number == 10 { 0 } else { number };
97 shown.to_string()
98}
99
100pub(crate) struct Pen<'a> {
101 buf: &'a mut Buffer,
102 pub(crate) x: u16,
103 y: u16,
104 right: u16,
105}
106
107impl<'a> Pen<'a> {
108 pub(crate) fn new(buf: &'a mut Buffer, line: Rect, x: u16) -> Option<Self> {
109 let area = line.intersection(buf.area);
110 if area.is_empty() {
111 return None;
112 }
113 Some(Pen {
114 buf,
115 x: x.clamp(area.left(), area.right()),
116 y: area.y,
117 right: area.right(),
118 })
119 }
120
121 fn room(&self) -> u16 {
122 self.right.saturating_sub(self.x)
123 }
124
125 pub(crate) fn put(&mut self, text: &str, style: Style) {
126 self.putn(text, style, self.room());
127 }
128
129 fn putn(&mut self, text: &str, style: Style, room: u16) {
130 let room = room.min(self.room());
131 if room == 0 || text.is_empty() {
132 return;
133 }
134 let (end, _) = self
135 .buf
136 .set_stringn(self.x, self.y, text, usize::from(room), style);
137 self.x = end;
138 }
139
140 pub(crate) fn fill_to(&mut self, to: u16, symbol: &str, style: Style) {
141 let to = to.min(self.right);
142 while self.x < to {
143 self.buf[(self.x, self.y)]
144 .set_symbol(symbol)
145 .set_style(style);
146 self.x += 1;
147 }
148 }
149
150 pub(crate) fn fill(&mut self, symbol: &str, style: Style) {
151 self.fill_to(self.right, symbol, style);
152 }
153
154 fn spans<T: AsRef<str>>(&mut self, spans: &[(T, Style)], base: Style, room: u16) {
155 let room = room.min(self.room());
156 if spans_width(spans) <= room {
157 for (text, style) in spans {
158 self.put(text.as_ref(), base.patch(*style));
159 }
160 return;
161 }
162 if room == 0 {
163 return;
164 }
165 let stop = self.x + room - 1;
166 let mut last = base;
167 for (text, style) in spans {
168 let style = base.patch(*style);
169 if self.x >= stop {
170 break;
171 }
172 last = style;
173 self.putn(text.as_ref(), style, stop - self.x);
174 }
175 self.x = self.x.min(stop);
176 self.put(ELLIPSIS, last);
177 }
178}
179
180fn label<T: AsRef<str>>(pen: &mut Pen, spans: &[(T, Style)], base: Style, room: u16) {
181 if spans.is_empty() {
182 return;
183 }
184 let first = base.patch(spans[0].1);
185 let last = base.patch(spans[spans.len() - 1].1);
186 pen.put(" ", first);
187 pen.spans(spans, base, room);
188 pen.put(" ", last);
189}
190
191pub(crate) struct Rule<'a> {
192 pub(crate) rule: Style,
193 pub(crate) base: Style,
194 pub(crate) middle: &'a [Span],
195 pub(crate) left: &'a [(String, Style)],
196 pub(crate) right: &'a [(String, Style)],
197}
198
199impl Rule<'_> {
200 pub(crate) fn draw(&self, buf: &mut Buffer, line: Rect) {
201 let Some(mut pen) = Pen::new(buf, line, line.x) else {
202 return;
203 };
204 let wide = spans_width(self.middle).saturating_add(2).min(line.width);
205 let start = line.x.saturating_add((line.width - wide) / 2);
206 let side = start.saturating_sub(line.x).saturating_sub(2);
207 let sides = side >= SIDE_MIN;
208 let present = |parts: &[(String, Style)]| spans_width(parts) > 0;
209 if sides && present(self.left) {
210 pen.fill_to(line.x.saturating_add(1), RULE, self.rule);
211 label(&mut pen, self.left, Style::new(), side - 2);
212 }
213 pen.fill_to(start, RULE, self.rule);
214 if spans_width(self.middle) > 0 {
215 label(&mut pen, self.middle, self.base, wide.saturating_sub(2));
216 }
217 if sides && present(self.right) {
218 let wide = spans_width(self.right).min(side - 2).saturating_add(2);
219 pen.fill_to(line.right().saturating_sub(1 + wide), RULE, self.rule);
220 label(&mut pen, self.right, Style::new(), wide - 2);
221 }
222 pen.fill(RULE, self.rule);
223 }
224}
225
226#[derive(Debug, Clone, Copy, PartialEq, Eq)]
227pub(crate) enum Kind {
228 Groups,
229 Sections,
230}
231
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233enum Form {
234 Full,
235 Short,
236 Lone,
237 Bare,
238}
239
240const FORMS: [Form; 4] = [Form::Full, Form::Short, Form::Lone, Form::Bare];
241
242#[derive(Debug, Clone, Copy)]
243pub(crate) struct Item<'a> {
244 pub(crate) label: &'a Label,
245 pub(crate) number: Option<usize>,
246 pub(crate) on: bool,
247}
248
249fn first(spans: &[Span]) -> Option<(&str, Style)> {
250 let (text, style) = spans.iter().find(|(text, _)| !text.is_empty())?;
251 Some((text.graphemes(true).next()?, *style))
252}
253
254enum Body<'a> {
255 Spans(&'a [Span]),
256 Glyph(Option<(&'a str, Style)>),
257}
258
259impl Body<'_> {
260 fn width(&self) -> u16 {
261 match self {
262 Body::Spans(spans) => spans_width(spans),
263 Body::Glyph(glyph) => glyph.map_or(0, |(text, _)| width(text)),
264 }
265 }
266}
267
268impl<'a> Item<'a> {
269 fn body(&self, kind: Kind, form: Form) -> Body<'a> {
270 let short = self.label.short_spans_or_full();
271 let initial = || Body::Glyph(first(short));
272 let numbered = self.number.is_some() && kind == Kind::Sections;
273 match form {
274 Form::Full => Body::Spans(self.label.full()),
275 Form::Short => Body::Spans(short),
276 Form::Lone if self.on => Body::Spans(short),
277 Form::Lone | Form::Bare if numbered => Body::Glyph(None),
278 Form::Lone | Form::Bare => initial(),
279 }
280 }
281
282 fn width(&self, kind: Kind, form: Form) -> u16 {
283 let body = self.body(kind, form).width();
284 let number = self.number.map_or(0, |number| {
285 width(&digits(number)).saturating_add(u16::from(body > 0))
286 });
287 number.saturating_add(2).saturating_add(body)
288 }
289
290 fn draw(&self, pen: &mut Pen, kind: Kind, form: Form, style: Style) {
291 let body = self.body(kind, form);
292 pen.put(" ", style);
293 if let Some(number) = self.number {
294 pen.put(&digits(number), style);
295 if body.width() > 0 {
296 pen.put(" ", style);
297 }
298 }
299 match body {
300 Body::Spans(spans) => {
301 for (text, span) in spans {
302 pen.put(text, style.patch(*span));
303 }
304 }
305 Body::Glyph(Some((text, span))) => pen.put(text, style.patch(span)),
306 Body::Glyph(None) => {}
307 }
308 pen.put(" ", style);
309 }
310}
311
312pub(crate) struct Labels<'a> {
313 pub(crate) items: Vec<Item<'a>>,
314 pub(crate) kind: Kind,
315 pub(crate) on: Style,
316 pub(crate) off: Style,
317 pub(crate) tail: &'a [(String, Style)],
318 pub(crate) tail_style: Style,
319}
320
321impl Labels<'_> {
322 fn gap(&self) -> u16 {
323 match self.kind {
324 Kind::Groups => GROUP_GAP,
325 Kind::Sections => SECTION_GAP,
326 }
327 }
328
329 fn total(&self, form: Form, tail: bool) -> u16 {
330 let gap = self.gap();
331 let labels = self
332 .items
333 .iter()
334 .enumerate()
335 .fold(0u16, |total, (index, item)| {
336 let gap = if index > 0 { gap } else { 0 };
337 total
338 .saturating_add(gap)
339 .saturating_add(item.width(self.kind, form))
340 });
341 let tail = if tail { spans_width(self.tail) } else { 0 };
342 if tail == 0 {
343 return labels;
344 }
345 let gap = if labels > 0 { TAIL_GAP } else { 0 };
346 labels.saturating_add(gap).saturating_add(tail)
347 }
348
349 fn fit(&self, line: Rect) -> (Form, u16) {
350 for form in FORMS {
351 let total = self.total(form, true);
352 if total <= line.width {
353 return (form, line.x + (line.width - total) / 2);
354 }
355 }
356 (Form::Bare, line.x)
357 }
358
359 pub(crate) fn draw(&self, buf: &mut Buffer, line: Rect) {
360 let (form, start) = self.fit(line);
361 let Some(mut pen) = Pen::new(buf, line, start) else {
362 return;
363 };
364 let gap = self.gap();
365 for (index, item) in self.items.iter().enumerate() {
366 if index > 0 {
367 pen.x = pen.x.saturating_add(gap).min(pen.right);
368 }
369 let style = if item.on { self.on } else { self.off };
370 item.draw(&mut pen, self.kind, form, style);
371 }
372 if spans_width(self.tail) == 0 {
373 return;
374 }
375 if !self.items.is_empty() {
376 pen.x = pen.x.saturating_add(TAIL_GAP).min(pen.right);
377 }
378 let room = pen.room();
379 pen.spans(self.tail, self.tail_style, room);
380 }
381
382 pub(crate) fn hit(&self, line: Rect, column: u16) -> Option<usize> {
383 if column < line.x || column >= line.right() {
384 return None;
385 }
386 let (form, start) = self.fit(line);
387 let mut x = start;
388 for (index, item) in self.items.iter().enumerate() {
389 if index > 0 {
390 x = x.saturating_add(self.gap());
391 }
392 let end = x.saturating_add(item.width(self.kind, form));
393 if (x..end).contains(&column) {
394 return Some(index);
395 }
396 x = end;
397 }
398 None
399 }
400}
401
402pub(crate) fn blank(buf: &mut Buffer, line: Rect, style: Style) {
403 if let Some(mut pen) = Pen::new(buf, line, line.x) {
404 pen.fill(" ", style);
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use ratatui::style::Modifier;
412
413 fn row(buf: &Buffer, y: u16) -> String {
414 (0..buf.area.width)
415 .map(|x| buf[(x, y)].symbol().to_string())
416 .collect()
417 }
418
419 #[test]
420 fn numbers_restart_in_a_group_and_a_lone_tab_shows_none_with_its_slot_after() {
421 let home = Label::new("Home");
422 let next = Label::new("Next");
423 let accel = Label::spans([
424 ("M", Style::new().add_modifier(Modifier::UNDERLINED)),
425 ("ail", Style::new()),
426 ]);
427 let labels = Labels {
428 items: vec![
429 Item {
430 label: &home,
431 number: Some(1),
432 on: true,
433 },
434 Item {
435 label: &next,
436 number: Some(2),
437 on: false,
438 },
439 ],
440 kind: Kind::Sections,
441 on: Style::new().add_modifier(Modifier::BOLD),
442 off: Style::new(),
443 tail: &[("‹ all ›".to_string(), Style::new())],
444 tail_style: Style::new(),
445 };
446 let mut buf = Buffer::empty(Rect::new(0, 0, 30, 1));
447 labels.draw(&mut buf, Rect::new(0, 0, 30, 1));
448 assert_eq!(row(&buf, 0), " 1 Home 2 Next ‹ all › ");
449 assert_eq!(labels.hit(Rect::new(0, 0, 30, 1), 10), Some(1));
450 let lone = Labels {
451 items: vec![Item {
452 label: &accel,
453 number: None,
454 on: true,
455 }],
456 kind: Kind::Sections,
457 on: Style::new(),
458 off: Style::new(),
459 tail: &[],
460 tail_style: Style::new(),
461 };
462 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 1));
463 lone.draw(&mut buf, Rect::new(0, 0, 10, 1));
464 assert_eq!(row(&buf, 0), " Mail ");
465 assert!(buf[(3, 0)].modifier.contains(Modifier::UNDERLINED));
466 assert!(!buf[(4, 0)].modifier.contains(Modifier::UNDERLINED));
467 }
468}