Skip to main content

datui_lib/widgets/ui/
section_rule.rs

1//! A title on a rule: the way to divide space inside a Surface without a
2//! second border.
3
4use crate::render::context::RenderContext;
5use ratatui::buffer::Buffer;
6use ratatui::layout::Rect;
7use ratatui::style::{Modifier, Style};
8use ratatui::text::{Line, Span};
9use ratatui::widgets::{Paragraph, Widget};
10
11/// Never a focus signal: the section's focused row carries the rail and the
12/// accent, and the rule reads the same whether focus is in its section or not.
13pub struct SectionRule<'a> {
14    pub title: &'a str,
15    /// Optional flat chip after the title, usually a count.
16    pub chip: Option<&'a str>,
17}
18
19impl SectionRule<'_> {
20    pub fn render(&self, area: Rect, buf: &mut Buffer, ctx: &RenderContext) {
21        if area.height == 0 || area.width == 0 {
22            return;
23        }
24        let g = crate::glyphs::get();
25        let title_style = Style::default().fg(ctx.accent).add_modifier(Modifier::BOLD);
26        let rule_glyph = g.rule_h;
27        let rule_style = Style::default().fg(ctx.column_separator);
28
29        let mut spans = vec![Span::styled(self.title, title_style), Span::raw(" ")];
30        let mut used = crate::glyphs::display_width(self.title) + 1;
31        if let Some(chip) = self.chip {
32            let chip = format!(" {chip} ");
33            used += crate::glyphs::display_width(&chip) + 1;
34            spans.push(Span::styled(
35                chip,
36                Style::default().bg(ctx.controls_bg).fg(ctx.text_primary),
37            ));
38            spans.push(Span::raw(" "));
39        }
40        let rule_w = (area.width as usize).saturating_sub(used);
41        spans.push(Span::styled(rule_glyph.repeat(rule_w), rule_style));
42        Paragraph::new(Line::from(spans)).render(area, buf);
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    fn render(rule: &SectionRule, width: u16) -> String {
51        let ctx = RenderContext::for_test();
52        let area = Rect::new(0, 0, width, 1);
53        let mut buf = Buffer::empty(area);
54        rule.render(area, &mut buf, &ctx);
55        (0..width)
56            .map(|x| buf[(x, 0)].symbol().to_string())
57            .collect()
58    }
59
60    #[test]
61    fn the_rule_runs_from_the_title_to_the_edge() {
62        let g = crate::glyphs::get();
63        let out = render(
64            &SectionRule {
65                title: "Format",
66                chip: None,
67            },
68            20,
69        );
70        assert_eq!(out, format!("Format {}", g.rule_h.repeat(13)));
71    }
72
73    #[test]
74    fn a_chip_sits_between_title_and_rule() {
75        let out = render(
76            &SectionRule {
77                title: "Format",
78                chip: Some("6"),
79            },
80            20,
81        );
82        assert!(out.starts_with("Format  6 "), "got {out:?}");
83    }
84
85    /// The rule is plain: no heavy glyph and no bright accent, which once
86    /// marked the focused section beside the rail.
87    #[test]
88    fn the_rule_never_signals_focus() {
89        let g = crate::glyphs::get();
90        let ctx = RenderContext::for_test();
91        let area = Rect::new(0, 0, 20, 1);
92        let mut buf = Buffer::empty(area);
93        SectionRule {
94            title: "Sort",
95            chip: None,
96        }
97        .render(area, &mut buf, &ctx);
98        assert_eq!(buf[(0, 0)].fg, ctx.accent);
99        assert_eq!(buf[(19, 0)].symbol(), g.rule_h);
100        assert_eq!(buf[(19, 0)].fg, ctx.column_separator);
101    }
102}