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}