1use ctl_text::{Context, Line, Measurement, Overflow, Renderable, Text};
4
5use crate::glyphs::Glyphs;
6use crate::lines::{literal, rule_line};
7
8const TITLE_MARGIN: u16 = 4;
11
12#[derive(Clone, Debug, Default)]
28pub struct Rule {
29 title: Option<Text>,
30}
31
32impl Rule {
33 #[must_use]
35 pub fn new() -> Self {
36 Self::default()
37 }
38
39 #[must_use]
42 pub fn titled(title: Text) -> Self {
43 Self { title: Some(title) }
44 }
45}
46
47impl Renderable for Rule {
48 fn measure(&self, context: &Context) -> Measurement {
49 let Some(title) = &self.title else {
50 return Measurement::exact(1);
51 };
52 let measured = title.measure_as(context, Overflow::Ellipsis);
53 Measurement::new(
54 measured.minimum().saturating_add(TITLE_MARGIN),
55 measured.maximum().saturating_add(TITLE_MARGIN),
56 )
57 }
58
59 fn render(&self, context: &Context) -> Vec<Line> {
60 let rule_char = Glyphs::for_chars(context.box_chars()).rule;
61 let width = context.width();
62 let titled = self.title.as_ref().and_then(|title| {
63 let room = width.saturating_sub(TITLE_MARGIN);
64 if room == 0 {
65 return None;
66 }
67 title
68 .render_as(&context.at_width(room), Overflow::Ellipsis)
69 .into_iter()
70 .find(|line| !line.is_empty())
71 });
72 let Some(title) = titled else {
73 return vec![rule_line(rule_char, width)];
74 };
75 let mut line = Line::new();
76 line.push(literal(&format!("{rule_char}{rule_char} ")));
77 line.extend(title.spans().iter().cloned());
78 let fill = width.saturating_sub(title.width().saturating_add(TITLE_MARGIN));
79 if fill > 0 {
80 let tail: String = std::iter::once(' ')
81 .chain(std::iter::repeat_n(rule_char, usize::from(fill)))
82 .collect();
83 line.push(literal(&tail));
84 }
85 vec![line]
86 }
87}