Skip to main content

pito_header/
nav.rs

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}