Skip to main content

pito_header/
header.rs

1use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
2
3use crate::nav::{Breadcrumb, Nav, NavBar, Place, RULE};
4use crate::styles::Styles;
5use crate::text::{self, ELLIPSIS, Pen};
6
7const SEPARATOR: &str = " ยท ";
8const SIDE_MIN: u16 = 6;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct Fact<'a> {
12    pub text: &'a str,
13    pub style: Style,
14}
15
16impl<'a> Fact<'a> {
17    pub const fn new(text: &'a str, style: Style) -> Self {
18        Fact { text, style }
19    }
20}
21
22#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
23enum Slot<'a> {
24    #[default]
25    Empty,
26    One(Fact<'a>),
27    Parts(&'a [Fact<'a>]),
28    Pairs(&'a [(String, Style)]),
29}
30
31impl<'a> Slot<'a> {
32    fn parts(&self) -> Parts<'a> {
33        match *self {
34            Slot::Empty => Parts::Slice(&[]),
35            Slot::One(fact) => Parts::One(fact),
36            Slot::Parts(parts) => Parts::Slice(parts),
37            Slot::Pairs(pairs) => Parts::Pairs(pairs),
38        }
39    }
40
41    fn width(&self) -> u16 {
42        self.parts().width()
43    }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47enum Parts<'a> {
48    One(Fact<'a>),
49    Slice(&'a [Fact<'a>]),
50    Pairs(&'a [(String, Style)]),
51}
52
53impl<'a> Parts<'a> {
54    fn is_empty(&self) -> bool {
55        self.len() == 0
56    }
57
58    fn len(&self) -> usize {
59        match self {
60            Parts::One(_) => 1,
61            Parts::Slice(parts) => parts.len(),
62            Parts::Pairs(pairs) => pairs.len(),
63        }
64    }
65
66    fn get(&self, index: usize) -> Fact<'a> {
67        match *self {
68            Parts::One(fact) => fact,
69            Parts::Slice(parts) => parts[index],
70            Parts::Pairs(pairs) => Fact::new(&pairs[index].0, pairs[index].1),
71        }
72    }
73
74    fn iter(&self) -> impl Iterator<Item = Fact<'a>> + Clone {
75        let parts = *self;
76        (0..parts.len()).map(move |index| parts.get(index))
77    }
78
79    fn width(&self) -> u16 {
80        self.iter().fold(0u16, |total, part| {
81            total.saturating_add(text::width(part.text))
82        })
83    }
84}
85
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum Drill {
89    #[default]
90    Replace,
91    Rows,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub struct Header<'a> {
96    nav: &'a Nav,
97    styles: Styles,
98    title: Option<&'a str>,
99    left: Slot<'a>,
100    right: Slot<'a>,
101    tabs: bool,
102    lit: bool,
103    underline: bool,
104    facts: Slot<'a>,
105    facts_rule: bool,
106    separator: &'a str,
107    crumbs: bool,
108    closing: bool,
109    notice: Option<Fact<'a>>,
110    drill: Drill,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114struct Rows {
115    title: Option<Rect>,
116    tabs: Option<Rect>,
117    facts: Option<Rect>,
118    crumbs: Option<Rect>,
119    closing: Option<Rect>,
120    notice: Option<Rect>,
121}
122
123impl<'a> Header<'a> {
124    pub fn new(nav: &'a Nav) -> Self {
125        Header {
126            nav,
127            styles: Styles::new(),
128            title: None,
129            left: Slot::Empty,
130            right: Slot::Empty,
131            tabs: true,
132            lit: true,
133            underline: false,
134            facts: Slot::Empty,
135            facts_rule: false,
136            separator: SEPARATOR,
137            crumbs: false,
138            closing: false,
139            notice: None,
140            drill: Drill::Replace,
141        }
142    }
143
144    pub fn styles(mut self, styles: Styles) -> Self {
145        self.styles = styles;
146        self
147    }
148
149    pub fn title(mut self, title: Option<&'a str>) -> Self {
150        self.title = title.filter(|title| !title.is_empty());
151        self
152    }
153
154    pub fn left(mut self, left: Option<Fact<'a>>) -> Self {
155        self.left = slot(left.map_or(Slot::Empty, Slot::One));
156        self
157    }
158
159    pub fn right(mut self, right: Option<Fact<'a>>) -> Self {
160        self.right = slot(right.map_or(Slot::Empty, Slot::One));
161        self
162    }
163
164    pub fn left_parts(mut self, parts: &'a [Fact<'a>]) -> Self {
165        self.left = slot(Slot::Parts(parts));
166        self
167    }
168
169    pub fn right_parts(mut self, parts: &'a [Fact<'a>]) -> Self {
170        self.right = slot(Slot::Parts(parts));
171        self
172    }
173
174    pub fn left_pairs(mut self, parts: &'a [(String, Style)]) -> Self {
175        self.left = slot(Slot::Pairs(parts));
176        self
177    }
178
179    pub fn right_pairs(mut self, parts: &'a [(String, Style)]) -> Self {
180        self.right = slot(Slot::Pairs(parts));
181        self
182    }
183
184    pub fn tabs(mut self, tabs: bool) -> Self {
185        self.tabs = tabs;
186        self
187    }
188
189    pub fn lit(mut self, lit: bool) -> Self {
190        self.lit = lit;
191        self
192    }
193
194    pub fn underline(mut self, underline: bool) -> Self {
195        self.underline = underline;
196        self
197    }
198
199    pub fn facts(mut self, facts: &'a [Fact<'a>]) -> Self {
200        self.facts = Slot::Parts(facts);
201        self
202    }
203
204    pub fn facts_pairs(mut self, facts: &'a [(String, Style)]) -> Self {
205        self.facts = Slot::Pairs(facts);
206        self
207    }
208
209    pub fn facts_rule(mut self, facts_rule: bool) -> Self {
210        self.facts_rule = facts_rule;
211        self
212    }
213
214    pub fn separator(mut self, separator: &'a str) -> Self {
215        self.separator = separator;
216        self
217    }
218
219    pub fn crumbs(mut self, crumbs: bool) -> Self {
220        self.crumbs = crumbs;
221        self
222    }
223
224    pub fn closing(mut self, closing: bool) -> Self {
225        self.closing = closing;
226        self
227    }
228
229    pub fn notice(mut self, notice: Option<Fact<'a>>) -> Self {
230        self.notice = notice.filter(|fact| !fact.text.is_empty());
231        self
232    }
233
234    pub fn drill(mut self, drill: Drill) -> Self {
235        self.drill = drill;
236        self
237    }
238
239    pub fn height(&self) -> u16 {
240        let replaced = self.replaced();
241        u16::from(self.title.is_some())
242            + if self.tabs { self.bar().height() } else { 0 }
243            + u16::from(self.has_facts() && !replaced)
244            + u16::from(self.crumbs && !replaced)
245            + u16::from(self.closing)
246            + u16::from(self.notice.is_some())
247    }
248
249    pub fn hit(&self, area: Rect, column: u16, row: u16) -> Option<Place> {
250        let tabs = self.rows(area).tabs?;
251        let bar = self.bar();
252        if self.replaced() && bar.rows(tabs).1.is_some_and(|line| line.y == row) {
253            return None;
254        }
255        bar.hit(tabs, column, row)
256    }
257
258    fn has_facts(&self) -> bool {
259        self.facts.parts().iter().any(|fact| !fact.text.is_empty())
260    }
261
262    fn replaced(&self) -> bool {
263        self.drill == Drill::Replace && self.tabs && self.nav.depth() > 0
264    }
265
266    fn bar(&self) -> NavBar<'a> {
267        NavBar::new(self.nav)
268            .styles(self.styles)
269            .lit(self.lit)
270            .underline(self.underline)
271    }
272
273    fn rows(&self, area: Rect) -> Rows {
274        let mut y = area.y;
275        let mut take = |on: bool, height: u16| {
276            if !on || height == 0 || y >= area.bottom() {
277                return None;
278            }
279            let height = height.min(area.bottom() - y);
280            let row = Rect::new(area.x, y, area.width, height);
281            y += height;
282            Some(row)
283        };
284        Rows {
285            title: take(self.title.is_some(), 1),
286            tabs: take(self.tabs, self.bar().height()),
287            facts: take(self.has_facts() && !self.replaced(), 1),
288            crumbs: take(self.crumbs && !self.replaced(), 1),
289            closing: take(self.closing, 1),
290            notice: take(self.notice.is_some(), 1),
291        }
292    }
293
294    fn draw_title(&self, buf: &mut Buffer, line: Rect, title: &str) {
295        let Some(mut pen) = Pen::new(buf, line, line.x, line.y) else {
296            return;
297        };
298        let rule = self.styles.rule;
299        let lit = self.styles.lit();
300        let wide = text::width(title).saturating_add(2).min(line.width);
301        let start = line.x.saturating_add((line.width - wide) / 2);
302        let side = start.saturating_sub(line.x).saturating_sub(2);
303        let sides = side >= SIDE_MIN;
304        if sides && self.left != Slot::Empty {
305            pen.fill_to(line.x.saturating_add(1), RULE, rule);
306            label(&mut pen, self.left.parts(), side - 2);
307        }
308        pen.fill_to(start, RULE, rule);
309        label(
310            &mut pen,
311            Parts::One(Fact::new(title, lit)),
312            wide.saturating_sub(2),
313        );
314        if sides && self.right != Slot::Empty {
315            let wide = self.right.width().min(side - 2).saturating_add(2);
316            pen.fill_to(line.right().saturating_sub(1 + wide), RULE, rule);
317            label(&mut pen, self.right.parts(), wide - 2);
318        }
319        pen.fill(RULE, rule);
320    }
321
322    fn draw_tabs(&self, buf: &mut Buffer, tabs: Rect) {
323        let bar = self.bar();
324        if !self.replaced() {
325            return bar.render(tabs, buf);
326        }
327        let (groups, sections) = bar.rows(tabs);
328        if let Some(line) = groups {
329            bar.draw_groups(buf, line);
330        }
331        if let Some(line) = sections {
332            Breadcrumb::new(self.nav)
333                .styles(self.styles)
334                .centred(true)
335                .render(line, buf);
336        }
337    }
338
339    fn draw_facts(&self, buf: &mut Buffer, line: Rect) {
340        let pad = if self.facts_rule { 1 } else { 0 };
341        let separator = text::width(self.separator);
342        let facts = self.facts.parts();
343        let present = || facts.iter().filter(|fact| !fact.text.is_empty());
344        let inner = present().enumerate().fold(0u16, |total, (index, fact)| {
345            let gap = if index > 0 { separator } else { 0 };
346            total
347                .saturating_add(gap)
348                .saturating_add(text::width(fact.text))
349        });
350        let shown = inner.min(line.width.saturating_sub(4 * pad));
351        let block = shown.saturating_add(2 * pad);
352        let start = line.x.saturating_add(line.width.saturating_sub(block) / 2);
353        let rule = self.styles.rule;
354        let Some(mut pen) = Pen::new(buf, line, line.x, line.y) else {
355            return;
356        };
357        if self.facts_rule {
358            pen.fill_to(start, RULE, rule);
359        } else {
360            pen.skip(start.saturating_sub(line.x));
361        }
362        let muted = self.styles.muted;
363        if pad > 0 {
364            pen.put(" ", muted);
365        }
366        let end = pen.x.saturating_add(shown);
367        for (index, fact) in present().enumerate() {
368            let wide = text::width(fact.text);
369            if index > 0 {
370                let room = end.saturating_sub(pen.x);
371                if room < separator.saturating_add(wide.min(2)) {
372                    pen.clip(ELLIPSIS, muted, room);
373                    break;
374                }
375                pen.put(self.separator, muted);
376            }
377            let room = end.saturating_sub(pen.x);
378            pen.clip(fact.text, fact.style, room);
379            if wide > room {
380                break;
381            }
382        }
383        if pad > 0 {
384            pen.put(" ", muted);
385            pen.fill(RULE, rule);
386        }
387    }
388
389    fn draw_notice(&self, buf: &mut Buffer, line: Rect, notice: Fact) {
390        if let Some(mut pen) = Pen::new(buf, line, line.x, line.y) {
391            let room = pen.room();
392            pen.clip(notice.text, notice.style, room);
393        }
394    }
395}
396
397fn slot(slot: Slot) -> Slot {
398    if slot.width() == 0 { Slot::Empty } else { slot }
399}
400
401fn label(pen: &mut Pen, parts: Parts, room: u16) {
402    if parts.is_empty() {
403        return;
404    }
405    let first = parts.get(0).style;
406    let last = parts.get(parts.len() - 1).style;
407    pen.put(" ", first);
408    if parts.width() <= room {
409        for part in parts.iter() {
410            pen.put(part.text, part.style);
411        }
412    } else if room > 0 {
413        let stop = pen.x.saturating_add(room - 1);
414        let mut style = first;
415        for part in parts.iter().filter(|part| !part.text.is_empty()) {
416            style = part.style;
417            if pen.x >= stop {
418                break;
419            }
420            let start = pen.x;
421            pen.putn(part.text, part.style, stop - pen.x);
422            if pen.x - start < text::width(part.text) {
423                break;
424            }
425        }
426        pen.x = pen.x.min(stop);
427        pen.put(ELLIPSIS, style);
428    }
429    pen.put(" ", last);
430}
431
432impl Widget for Header<'_> {
433    fn render(self, area: Rect, buf: &mut Buffer) {
434        (&self).render(area, buf);
435    }
436}
437
438impl Widget for &Header<'_> {
439    fn render(self, area: Rect, buf: &mut Buffer) {
440        let rows = self.rows(area);
441        if let (Some(line), Some(title)) = (rows.title, self.title) {
442            self.draw_title(buf, line, title);
443        }
444        if let Some(tabs) = rows.tabs {
445            self.draw_tabs(buf, tabs);
446        }
447        if let Some(line) = rows.facts {
448            self.draw_facts(buf, line);
449        }
450        if let Some(line) = rows.crumbs {
451            Breadcrumb::new(self.nav)
452                .styles(self.styles)
453                .render(line, buf);
454        }
455        if let Some(line) = rows.closing
456            && let Some(mut pen) = Pen::new(buf, line, line.x, line.y)
457        {
458            pen.fill(RULE, self.styles.rule);
459        }
460        if let (Some(line), Some(notice)) = (rows.notice, self.notice) {
461            self.draw_notice(buf, line, notice);
462        }
463    }
464}