1use std::borrow::Cow;
2
3use ratatui::{
4 buffer::Buffer,
5 layout::Rect,
6 style::{Modifier, Style},
7 widgets::Widget,
8};
9
10use crate::key::Key;
11use crate::styles::Styles;
12use crate::text::{self, ELLIPSIS, Pen};
13
14const GROUP_GAP: u16 = 2;
15const SECTION_GAP: u16 = 0;
16const JOIN: &str = " / ";
17pub(crate) const RULE: &str = "─";
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20struct Names {
21 name: Cow<'static, str>,
22 short: Option<Cow<'static, str>>,
23}
24
25impl Names {
26 fn new(name: impl Into<Cow<'static, str>>) -> Self {
27 Names {
28 name: name.into(),
29 short: None,
30 }
31 }
32
33 fn short_name(&self) -> &str {
34 self.short.as_deref().unwrap_or(&self.name)
35 }
36}
37
38type Span = (Cow<'static, str>, Style);
39
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Section {
42 names: Names,
43 spans: Vec<Span>,
44 short_spans: Vec<Span>,
45}
46
47fn owned(spans: &[(&str, Style)]) -> Vec<Span> {
48 spans
49 .iter()
50 .map(|(text, style)| (Cow::Owned(text.to_string()), *style))
51 .collect()
52}
53
54fn joined(spans: &[(&str, Style)]) -> String {
55 spans.iter().map(|(text, _)| *text).collect()
56}
57
58impl Section {
59 pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
60 Section {
61 names: Names::new(name),
62 spans: Vec::new(),
63 short_spans: Vec::new(),
64 }
65 }
66
67 pub fn spans(spans: &[(&str, Style)]) -> Self {
68 Section {
69 names: Names::new(joined(spans)),
70 spans: owned(spans),
71 short_spans: Vec::new(),
72 }
73 }
74
75 pub fn short(mut self, short: impl Into<Cow<'static, str>>) -> Self {
76 self.names.short = Some(short.into());
77 self.short_spans.clear();
78 self
79 }
80
81 pub fn short_spans(mut self, spans: &[(&str, Style)]) -> Self {
82 self.names.short = Some(Cow::Owned(joined(spans)));
83 self.short_spans = owned(spans);
84 self
85 }
86
87 pub fn set_name(&mut self, name: impl Into<Cow<'static, str>>) {
88 self.names.name = name.into();
89 self.spans.clear();
90 }
91
92 pub fn set_spans(&mut self, spans: &[(&str, Style)]) {
93 self.names.name = Cow::Owned(joined(spans));
94 self.spans = owned(spans);
95 }
96
97 pub fn set_short(&mut self, short: impl Into<Cow<'static, str>>) {
98 self.names.short = Some(short.into());
99 self.short_spans.clear();
100 }
101
102 pub fn set_short_spans(&mut self, spans: &[(&str, Style)]) {
103 self.names.short = Some(Cow::Owned(joined(spans)));
104 self.short_spans = owned(spans);
105 }
106
107 pub fn name(&self) -> &str {
108 &self.names.name
109 }
110
111 pub fn short_name(&self) -> &str {
112 self.names.short_name()
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct Group {
118 names: Names,
119 sections: Vec<Section>,
120}
121
122impl Group {
123 pub fn new(name: impl Into<Cow<'static, str>>) -> Self {
124 Group {
125 names: Names::new(name),
126 sections: Vec::new(),
127 }
128 }
129
130 pub fn short(mut self, short: impl Into<Cow<'static, str>>) -> Self {
131 self.names.short = Some(short.into());
132 self
133 }
134
135 pub fn section(mut self, section: Section) -> Self {
136 self.sections.push(section);
137 self
138 }
139
140 pub fn name(&self) -> &str {
141 &self.names.name
142 }
143
144 pub fn short_name(&self) -> &str {
145 self.names.short_name()
146 }
147
148 pub fn sections(&self) -> &[Section] {
149 &self.sections
150 }
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct Place {
155 pub group: usize,
156 pub section: usize,
157 pub number: usize,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum Action {
163 NextGroup,
164 PrevGroup,
165 NextSection,
166 PrevSection,
167 Go(usize),
168 Back,
169 Swallow,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
173#[non_exhaustive]
174pub struct NavKeys {
175 pub next_group: &'static [Key],
176 pub prev_group: &'static [Key],
177 pub next_section: &'static [Key],
178 pub prev_section: &'static [Key],
179 pub back: &'static [Key],
180 pub digits: bool,
181 pub swallow_digits: bool,
182}
183
184impl NavKeys {
185 pub const HEY: NavKeys = NavKeys {
186 next_group: &[Key::Tab],
187 prev_group: &[Key::BackTab],
188 next_section: &[Key::Char(']')],
189 prev_section: &[Key::Char('[')],
190 back: &[Key::Esc, Key::Char('q')],
191 digits: true,
192 swallow_digits: false,
193 };
194
195 pub const NONE: NavKeys = NavKeys {
196 next_group: &[],
197 prev_group: &[],
198 next_section: &[],
199 prev_section: &[],
200 back: &[],
201 digits: false,
202 swallow_digits: false,
203 };
204
205 pub const fn next_group(mut self, keys: &'static [Key]) -> Self {
206 self.next_group = keys;
207 self
208 }
209
210 pub const fn prev_group(mut self, keys: &'static [Key]) -> Self {
211 self.prev_group = keys;
212 self
213 }
214
215 pub const fn next_section(mut self, keys: &'static [Key]) -> Self {
216 self.next_section = keys;
217 self
218 }
219
220 pub const fn prev_section(mut self, keys: &'static [Key]) -> Self {
221 self.prev_section = keys;
222 self
223 }
224
225 pub const fn back(mut self, keys: &'static [Key]) -> Self {
226 self.back = keys;
227 self
228 }
229
230 pub const fn digits(mut self, on: bool) -> Self {
231 self.digits = on;
232 self
233 }
234
235 pub const fn swallow_digits(mut self, on: bool) -> Self {
236 self.swallow_digits = on;
237 self
238 }
239}
240
241impl Default for NavKeys {
242 fn default() -> Self {
243 NavKeys::HEY
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248#[non_exhaustive]
249pub enum Step {
250 Moved(Place),
251 Back { selected: usize },
252 Swallowed,
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
256struct Level {
257 title: Cow<'static, str>,
258 selected: usize,
259}
260
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub struct Nav {
263 groups: Vec<Group>,
264 group: usize,
265 last: Vec<usize>,
266 stacks: Vec<Vec<Level>>,
267 keys: NavKeys,
268}
269
270impl Nav {
271 pub fn new(groups: Vec<Group>) -> Self {
272 let total = groups.iter().map(|group| group.sections.len()).sum();
273 let group = groups
274 .iter()
275 .position(|group| !group.sections.is_empty())
276 .unwrap_or(0);
277 Nav {
278 last: vec![0; groups.len()],
279 stacks: vec![Vec::new(); total],
280 groups,
281 group,
282 keys: NavKeys::HEY,
283 }
284 }
285
286 pub fn keys(mut self, keys: NavKeys) -> Self {
287 self.keys = keys;
288 self
289 }
290
291 pub fn groups(&self) -> &[Group] {
292 &self.groups
293 }
294
295 pub fn place(&self) -> Place {
296 let section = self.last.get(self.group).copied().unwrap_or(0);
297 let filled = self
298 .groups
299 .get(self.group)
300 .is_some_and(|group| !group.sections.is_empty());
301 Place {
302 group: self.group,
303 section,
304 number: if filled {
305 self.offset(self.group) + section + 1
306 } else {
307 0
308 },
309 }
310 }
311
312 pub fn selected(&self, group: usize) -> Option<usize> {
313 let section = *self.last.get(group)?;
314 (section < self.groups[group].sections.len()).then_some(section)
315 }
316
317 pub fn section(&self) -> Option<&Section> {
318 let place = self.place();
319 self.groups.get(place.group)?.sections.get(place.section)
320 }
321
322 pub fn section_mut(&mut self, group: usize, section: usize) -> Option<&mut Section> {
323 self.groups.get_mut(group)?.sections.get_mut(section)
324 }
325
326 pub fn count(&self) -> usize {
327 self.stacks.len()
328 }
329
330 pub fn go(&mut self, number: usize) -> Option<Place> {
331 if number == 0 {
332 return None;
333 }
334 let mut offset = 0;
335 for (index, group) in self.groups.iter().enumerate() {
336 let len = group.sections.len();
337 if number <= offset + len {
338 self.group = index;
339 self.last[index] = number - offset - 1;
340 return Some(self.place());
341 }
342 offset += len;
343 }
344 None
345 }
346
347 pub fn go_to(&mut self, group: usize, section: usize) -> Option<Place> {
348 let len = self.groups.get(group)?.sections.len();
349 if section >= len {
350 return None;
351 }
352 self.group = group;
353 self.last[group] = section;
354 Some(self.place())
355 }
356
357 pub fn cycle(&mut self, by: isize) -> Option<Place> {
358 if by == 0 {
359 return Some(self.place());
360 }
361 let filled = self
362 .groups
363 .iter()
364 .filter(|group| !group.sections.is_empty())
365 .count();
366 if filled < 2 {
367 return None;
368 }
369 let step = if by < 0 { -1 } else { 1 };
370 let count = self.groups.len() as isize;
371 let mut index = self.group as isize;
372 for _ in 0..by.unsigned_abs() % filled {
373 loop {
374 index = (index + step).rem_euclid(count);
375 if !self.groups[index as usize].sections.is_empty() {
376 break;
377 }
378 }
379 }
380 self.group = index as usize;
381 Some(self.place())
382 }
383
384 pub fn step(&mut self, by: isize) -> Option<Place> {
385 let len = self.groups.get(self.group)?.sections.len() as isize;
386 if len == 0 {
387 return None;
388 }
389 let at = self.last[self.group] as isize;
390 self.last[self.group] = (at + by.rem_euclid(len)).rem_euclid(len) as usize;
391 Some(self.place())
392 }
393
394 pub fn open(&mut self, title: impl Into<Cow<'static, str>>, selected: usize) {
395 if let Some(stack) = self.stack_mut() {
396 stack.push(Level {
397 title: title.into(),
398 selected,
399 });
400 }
401 }
402
403 pub fn back(&mut self) -> Option<usize> {
404 self.stack_mut()?.pop().map(|level| level.selected)
405 }
406
407 pub fn close(&mut self) -> Option<usize> {
408 let stack = self.stack_mut()?;
409 let selected = stack.first().map(|level| level.selected);
410 stack.clear();
411 selected
412 }
413
414 pub fn depth(&self) -> usize {
415 self.stack().map_or(0, Vec::len)
416 }
417
418 pub fn title(&self) -> Option<&str> {
419 self.stack()?.last().map(|level| level.title.as_ref())
420 }
421
422 pub fn crumbs(&self) -> impl Iterator<Item = &str> {
423 self.section().map(Section::name).into_iter().chain(
424 self.stack()
425 .into_iter()
426 .flatten()
427 .map(|level| level.title.as_ref()),
428 )
429 }
430
431 pub fn breadcrumb(&self) -> String {
432 let mut out = String::new();
433 for (index, crumb) in self.crumbs().enumerate() {
434 if index > 0 {
435 out.push_str(JOIN);
436 }
437 out.push_str(crumb);
438 }
439 out
440 }
441
442 pub fn action(&self, key: Key) -> Option<Action> {
443 let keys = &self.keys;
444 let filled = self
445 .groups
446 .iter()
447 .filter(|group| !group.sections.is_empty())
448 .count();
449 let action = if keys.next_group.contains(&key) {
450 if filled > 1 {
451 Action::NextGroup
452 } else {
453 Action::NextSection
454 }
455 } else if keys.prev_group.contains(&key) {
456 if filled > 1 {
457 Action::PrevGroup
458 } else {
459 Action::PrevSection
460 }
461 } else if keys.next_section.contains(&key) {
462 Action::NextSection
463 } else if keys.prev_section.contains(&key) {
464 Action::PrevSection
465 } else if keys.back.contains(&key) {
466 if self.depth() == 0 {
467 return None;
468 }
469 Action::Back
470 } else {
471 match key {
472 Key::Char('0') if keys.digits => Action::Go(10),
473 Key::Char(digit @ '1'..='9') if keys.digits => {
474 Action::Go(digit as usize - '0' as usize)
475 }
476 _ => return None,
477 }
478 };
479 match action {
480 Action::Go(number) if number > self.count() && keys.swallow_digits => {
481 Some(Action::Swallow)
482 }
483 Action::Go(number) if number > self.count() => None,
484 Action::NextSection | Action::PrevSection if self.count() == 0 => None,
485 action => Some(action),
486 }
487 }
488
489 pub fn apply(&mut self, action: Action) -> Option<Step> {
490 let moved = match action {
491 Action::NextGroup => self.cycle(1),
492 Action::PrevGroup => self.cycle(-1),
493 Action::NextSection => self.step(1),
494 Action::PrevSection => self.step(-1),
495 Action::Go(number) => self.go(number),
496 Action::Back => return self.back().map(|selected| Step::Back { selected }),
497 Action::Swallow => return Some(Step::Swallowed),
498 };
499 moved.map(Step::Moved)
500 }
501
502 pub fn key(&mut self, key: Key) -> Option<Step> {
503 let action = self.action(key)?;
504 self.apply(action)
505 }
506
507 pub fn bar(&self) -> NavBar<'_> {
508 NavBar::new(self)
509 }
510
511 pub fn breadcrumb_bar(&self) -> Breadcrumb<'_> {
512 Breadcrumb::new(self)
513 }
514
515 fn offset(&self, group: usize) -> usize {
516 self.groups[..group]
517 .iter()
518 .map(|group| group.sections.len())
519 .sum()
520 }
521
522 fn flat(&self) -> Option<usize> {
523 let place = self.place();
524 (place.number > 0).then(|| place.number - 1)
525 }
526
527 fn stack(&self) -> Option<&Vec<Level>> {
528 self.stacks.get(self.flat()?)
529 }
530
531 fn stack_mut(&mut self) -> Option<&mut Vec<Level>> {
532 let flat = self.flat()?;
533 self.stacks.get_mut(flat)
534 }
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538enum Form {
539 Full,
540 Short,
541 Lone,
542 Bare,
543}
544
545const FORMS: [Form; 4] = [Form::Full, Form::Short, Form::Lone, Form::Bare];
546
547#[derive(Debug, Clone, Copy)]
548struct Label<'a> {
549 number: Option<usize>,
550 text: &'a str,
551 spans: &'a [Span],
552 on: bool,
553}
554
555impl Label<'_> {
556 fn shown(&self) -> Option<usize> {
557 self.number.map(|n| if n == 10 { 0 } else { n })
558 }
559
560 fn empty(&self) -> bool {
561 self.spans.is_empty() && self.text.is_empty()
562 }
563
564 fn body_width(&self) -> u16 {
565 if self.spans.is_empty() {
566 return text::width(self.text);
567 }
568 self.spans.iter().fold(0u16, |total, (text, _)| {
569 total.saturating_add(text::width(text))
570 })
571 }
572
573 fn width(&self) -> u16 {
574 let number = self
575 .shown()
576 .map_or(0, |n| text::number_width(n) + u16::from(!self.empty()));
577 number.saturating_add(2).saturating_add(self.body_width())
578 }
579
580 fn draw(&self, pen: &mut Pen, style: Style, underline: bool) {
581 let mut digits = [0; 20];
582 pen.put(" ", style);
583 if let Some(number) = self.shown() {
584 let number_style = if underline {
585 style.add_modifier(Modifier::UNDERLINED)
586 } else {
587 style
588 };
589 pen.put(text::digits(number, &mut digits), number_style);
590 if !self.empty() {
591 pen.put(" ", style);
592 }
593 }
594 if self.spans.is_empty() {
595 pen.put(self.text, style);
596 }
597 for (text, span) in self.spans {
598 pen.put(text, style.patch(*span));
599 }
600 pen.put(" ", style);
601 }
602}
603
604fn group_text(group: &Group, on: bool, form: Form) -> &str {
605 match form {
606 Form::Full => group.name(),
607 Form::Short => group.short_name(),
608 Form::Lone if on => group.short_name(),
609 Form::Lone | Form::Bare => text::first(group.short_name()),
610 }
611}
612
613fn section_body(section: &Section, on: bool, form: Form) -> (&str, &[Span]) {
614 let spans = section.spans.as_slice();
615 let short = if !section.short_spans.is_empty() {
616 ("", section.short_spans.as_slice())
617 } else if section.names.short.is_none() && !spans.is_empty() {
618 ("", spans)
619 } else {
620 (section.short_name(), &[][..])
621 };
622 match form {
623 Form::Full if !spans.is_empty() => ("", spans),
624 Form::Full => (section.name(), &[]),
625 Form::Short => short,
626 Form::Lone if on => short,
627 Form::Lone | Form::Bare => ("", &[]),
628 }
629}
630
631fn row_width<'a>(labels: impl Iterator<Item = Label<'a>>, gap: u16) -> u16 {
632 labels.enumerate().fold(0u16, |total, (index, label)| {
633 let gap = if index > 0 { gap } else { 0 };
634 total.saturating_add(gap).saturating_add(label.width())
635 })
636}
637
638#[derive(Debug, Clone, Copy, PartialEq, Eq)]
639pub struct NavBar<'a> {
640 nav: &'a Nav,
641 styles: Styles,
642 lit: bool,
643 groups: bool,
644 underline: bool,
645}
646
647impl<'a> NavBar<'a> {
648 pub fn new(nav: &'a Nav) -> Self {
649 NavBar {
650 nav,
651 styles: Styles::new(),
652 lit: true,
653 groups: nav.groups.len() > 1,
654 underline: false,
655 }
656 }
657
658 pub fn styles(mut self, styles: Styles) -> Self {
659 self.styles = styles;
660 self
661 }
662
663 pub fn lit(mut self, lit: bool) -> Self {
664 self.lit = lit;
665 self
666 }
667
668 pub fn groups(mut self, groups: bool) -> Self {
669 self.groups = groups;
670 self
671 }
672
673 pub fn underline(mut self, underline: bool) -> Self {
674 self.underline = underline;
675 self
676 }
677
678 pub fn height(&self) -> u16 {
679 u16::from(self.groups) + u16::from(self.nav.count() > 0)
680 }
681
682 pub fn hit(&self, area: Rect, column: u16, row: u16) -> Option<Place> {
683 if column < area.x || column >= area.right() {
684 return None;
685 }
686 let (groups, sections) = self.rows(area);
687 if let Some(line) = groups
688 && row == line.y
689 {
690 let (form, start) = self.fit_groups(line);
691 let index = hit_row(self.group_labels(form), GROUP_GAP, start, column)?;
692 let section = self.nav.last[index];
693 return (!self.nav.groups[index].sections.is_empty()).then(|| Place {
694 group: index,
695 section,
696 number: self.nav.offset(index) + section + 1,
697 });
698 }
699 let line = sections?;
700 if row != line.y {
701 return None;
702 }
703 let (form, start) = self.fit_sections(line);
704 let index = hit_row(self.section_labels(form), SECTION_GAP, start, column)?;
705 let group = self.nav.group;
706 Some(Place {
707 group,
708 section: index,
709 number: self.nav.offset(group) + index + 1,
710 })
711 }
712
713 pub(crate) fn rows(&self, area: Rect) -> (Option<Rect>, Option<Rect>) {
714 let line = |y: u16| Rect::new(area.x, y, area.width, 1);
715 let sections = self.nav.count() > 0;
716 match area.height {
717 0 => (None, None),
718 1 if sections => (None, Some(line(area.y))),
719 1 if self.groups => (Some(line(area.y)), None),
720 _ if self.groups && sections => (Some(line(area.y)), Some(line(area.y + 1))),
721 _ if self.groups => (Some(line(area.y)), None),
722 _ if sections => (None, Some(line(area.y))),
723 _ => (None, None),
724 }
725 }
726
727 fn group_labels(&self, form: Form) -> impl Iterator<Item = Label<'a>> + Clone {
728 let current = self.nav.group;
729 self.nav
730 .groups
731 .iter()
732 .enumerate()
733 .map(move |(index, group)| {
734 let on = index == current;
735 Label {
736 number: None,
737 text: group_text(group, on, form),
738 spans: &[],
739 on,
740 }
741 })
742 }
743
744 fn section_labels(&self, form: Form) -> impl Iterator<Item = Label<'a>> + Clone {
745 let place = self.nav.place();
746 let offset = self.nav.offset(place.group);
747 self.nav
748 .groups
749 .get(place.group)
750 .map(|group| group.sections.as_slice())
751 .unwrap_or(&[])
752 .iter()
753 .enumerate()
754 .map(move |(index, section)| {
755 let on = index == place.section;
756 let (text, spans) = section_body(section, on, form);
757 Label {
758 number: Some(offset + index + 1),
759 text,
760 spans,
761 on,
762 }
763 })
764 }
765
766 fn fit_groups(&self, line: Rect) -> (Form, u16) {
767 fit(|form| self.group_labels(form), GROUP_GAP, line)
768 }
769
770 fn fit_sections(&self, line: Rect) -> (Form, u16) {
771 fit(|form| self.section_labels(form), SECTION_GAP, line)
772 }
773}
774
775fn fit<'a, I>(labels: impl Fn(Form) -> I, gap: u16, line: Rect) -> (Form, u16)
776where
777 I: Iterator<Item = Label<'a>>,
778{
779 for form in FORMS {
780 let total = row_width(labels(form), gap);
781 if total <= line.width {
782 return (form, line.x.saturating_add((line.width - total) / 2));
783 }
784 }
785 (Form::Bare, line.x)
786}
787
788fn hit_row<'a>(
789 labels: impl Iterator<Item = Label<'a>>,
790 gap: u16,
791 start: u16,
792 column: u16,
793) -> Option<usize> {
794 let mut x = start;
795 for (index, label) in labels.enumerate() {
796 if index > 0 {
797 x = x.saturating_add(gap);
798 }
799 let end = x.saturating_add(label.width());
800 if (x..end).contains(&column) {
801 return Some(index);
802 }
803 x = end;
804 }
805 None
806}
807
808struct Row {
809 gap: u16,
810 start: u16,
811 on: Style,
812 off: Style,
813 underline: bool,
814}
815
816fn draw_row<'a>(buf: &mut Buffer, line: Rect, labels: impl Iterator<Item = Label<'a>>, row: Row) {
817 let Row {
818 gap,
819 start,
820 on,
821 off,
822 underline,
823 } = row;
824 let Some(mut pen) = Pen::new(buf, line, start, line.y) else {
825 return;
826 };
827 for (index, label) in labels.enumerate() {
828 if index > 0 {
829 pen.skip(gap);
830 }
831 label.draw(&mut pen, if label.on { on } else { off }, underline);
832 }
833}
834
835impl Widget for NavBar<'_> {
836 fn render(self, area: Rect, buf: &mut Buffer) {
837 (&self).render(area, buf);
838 }
839}
840
841impl Widget for &NavBar<'_> {
842 fn render(self, area: Rect, buf: &mut Buffer) {
843 let (groups, sections) = self.rows(area);
844 if let Some(line) = groups {
845 self.draw_groups(buf, line);
846 }
847 if let Some(line) = sections {
848 self.draw_sections(buf, line);
849 }
850 }
851}
852
853impl NavBar<'_> {
854 pub(crate) fn draw_groups(&self, buf: &mut Buffer, line: Rect) {
855 let (form, start) = self.fit_groups(line);
856 let row = Row {
857 gap: GROUP_GAP,
858 start,
859 on: self.styles.lit(),
860 off: self.styles.muted,
861 underline: false,
862 };
863 draw_row(buf, line, self.group_labels(form), row);
864 }
865
866 fn draw_sections(&self, buf: &mut Buffer, line: Rect) {
867 let (form, start) = self.fit_sections(line);
868 let muted = self.styles.muted;
869 let row = Row {
870 gap: SECTION_GAP,
871 start,
872 on: if self.lit { self.styles.lit() } else { muted },
873 off: muted,
874 underline: self.underline,
875 };
876 draw_row(buf, line, self.section_labels(form), row);
877 }
878}
879
880#[derive(Debug, Clone, Copy, PartialEq, Eq)]
881enum Trail {
882 Whole,
883 Ends,
884 Last,
885}
886
887impl Trail {
888 fn shown(self, index: usize, count: usize, crumb: &str) -> Option<&str> {
889 let last = index + 1 == count;
890 match self {
891 Trail::Whole => Some(crumb),
892 Trail::Ends if index == 0 || last => Some(crumb),
893 Trail::Ends if index == 1 => Some(ELLIPSIS),
894 Trail::Last if last => Some(crumb),
895 Trail::Last if index == 0 => Some(ELLIPSIS),
896 Trail::Ends | Trail::Last => None,
897 }
898 }
899}
900
901#[derive(Debug, Clone, Copy, PartialEq, Eq)]
902pub struct Breadcrumb<'a> {
903 nav: &'a Nav,
904 styles: Styles,
905 centred: bool,
906}
907
908impl<'a> Breadcrumb<'a> {
909 pub fn new(nav: &'a Nav) -> Self {
910 Breadcrumb {
911 nav,
912 styles: Styles::new(),
913 centred: false,
914 }
915 }
916
917 pub fn styles(mut self, styles: Styles) -> Self {
918 self.styles = styles;
919 self
920 }
921
922 pub fn centred(mut self, centred: bool) -> Self {
923 self.centred = centred;
924 self
925 }
926
927 fn trail_width(&self, trail: Trail) -> u16 {
928 let count = self.nav.crumbs().count();
929 let join = text::width(JOIN);
930 let mut first = true;
931 let mut total = 0u16;
932 for (index, crumb) in self.nav.crumbs().enumerate() {
933 if let Some(shown) = trail.shown(index, count, crumb) {
934 if !first {
935 total = total.saturating_add(join);
936 }
937 first = false;
938 total = total.saturating_add(text::width(shown));
939 }
940 }
941 total
942 }
943
944 fn draw_trail(&self, pen: &mut Pen, trail: Trail, room: u16, style: Style) {
945 let count = self.nav.crumbs().count();
946 let join = text::width(JOIN);
947 let mut first = true;
948 let mut elided = false;
949 let stop = pen.x.saturating_add(room);
950 for (index, crumb) in self.nav.crumbs().enumerate() {
951 let Some(shown) = trail.shown(index, count, crumb) else {
952 continue;
953 };
954 let left = stop.saturating_sub(pen.x);
955 let wide = text::width(shown);
956 if first {
957 pen.clip(shown, style, left);
958 if wide > left {
959 return;
960 }
961 } else if join.saturating_add(wide) <= left {
962 pen.put(JOIN, style);
963 pen.put(shown, style);
964 } else {
965 if left > join {
966 pen.put(JOIN, style);
967 pen.clip(shown, style, left - join);
968 } else if !elided {
969 pen.clip(ELLIPSIS, style, left);
970 }
971 return;
972 }
973 first = false;
974 elided = shown == ELLIPSIS;
975 }
976 }
977}
978
979impl Widget for Breadcrumb<'_> {
980 fn render(self, area: Rect, buf: &mut Buffer) {
981 (&self).render(area, buf);
982 }
983}
984
985impl Widget for &Breadcrumb<'_> {
986 fn render(self, area: Rect, buf: &mut Buffer) {
987 let style = self.styles.lit();
988 let rule = self.styles.rule;
989 let Some(mut pen) = Pen::new(buf, area, area.x, area.y) else {
990 return;
991 };
992 let room = area.width.saturating_sub(4);
993 if self.nav.crumbs().next().is_none() || room == 0 {
994 pen.fill(RULE, rule);
995 return;
996 }
997 let count = self.nav.crumbs().count();
998 let trail = [Trail::Whole, Trail::Ends, Trail::Last]
999 .into_iter()
1000 .filter(|trail| *trail != Trail::Ends || count > 2)
1001 .find(|trail| self.trail_width(*trail) <= room)
1002 .unwrap_or(Trail::Last);
1003 if self.centred {
1004 let block = self.trail_width(trail).min(room) + 2;
1005 pen.fill_to(
1006 area.x.saturating_add(area.width.saturating_sub(block) / 2),
1007 RULE,
1008 rule,
1009 );
1010 } else {
1011 pen.put(RULE, rule);
1012 }
1013 pen.put(" ", rule);
1014 self.draw_trail(&mut pen, trail, room, style);
1015 pen.put(" ", rule);
1016 pen.fill(RULE, rule);
1017 }
1018}