ctl_layout/
definitions.rs1use std::num::NonZeroU16;
4
5use ctl_text::{Context, Line, Measurement, Overflow, Renderable, Text};
6
7use crate::lines::{indent_lines, literal};
8
9const LABEL_INDENT: u16 = 2;
11const DESCRIPTION_INDENT: u16 = 4;
13
14#[derive(Clone, Debug, Default)]
31pub struct Definitions {
32 entries: Vec<Entry>,
33}
34
35#[derive(Clone, Debug)]
36struct Entry {
37 labels: Vec<Text>,
38 description: Vec<Text>,
39}
40
41impl Definitions {
42 #[must_use]
44 pub fn new() -> Self {
45 Self::default()
46 }
47
48 #[must_use]
52 pub fn entry<L, D>(
53 self,
54 labels: impl IntoIterator<Item = L>,
55 description: impl IntoIterator<Item = D>,
56 ) -> Self
57 where
58 L: Into<Text>,
59 D: Into<Text>,
60 {
61 self.push(
62 labels.into_iter().map(Into::into).collect(),
63 description.into_iter().map(Into::into).collect(),
64 )
65 }
66
67 fn push(mut self, labels: Vec<Text>, description: Vec<Text>) -> Self {
68 self.entries.push(Entry {
69 labels,
70 description,
71 });
72 self
73 }
74
75 fn lines(&self, context: &Context) -> Vec<Line> {
77 let (width, tab_width) = (context.width(), context.tab_width());
78 let mut lines = Vec::new();
79 for entry in &self.entries {
80 let labels = join_cells(&entry.labels, tab_width);
81 let description = join_cells(&entry.description, tab_width);
82 for (part, indent) in [(labels, LABEL_INDENT), (description, DESCRIPTION_INDENT)] {
83 if !part.is_empty() {
84 let fitted = part.fit(width.saturating_sub(indent), Overflow::KeepWords);
85 lines.extend(indent_lines(fitted, indent));
86 }
87 }
88 }
89 lines
90 }
91}
92
93fn join_cells(cells: &[Text], tab_width: NonZeroU16) -> Line {
95 let mut joined = Line::new();
96 for line in cells
97 .iter()
98 .flat_map(|cell| cell.lines(tab_width))
99 .filter(|line| !line.is_empty())
100 {
101 if !joined.is_empty() {
102 joined.push(literal(" "));
103 }
104 joined.extend(line.spans().iter().cloned());
105 }
106 joined
107}
108
109impl Renderable for Definitions {
110 fn measure(&self, context: &Context) -> Measurement {
111 let widest = |context: &Context| {
112 self.lines(context)
113 .iter()
114 .map(Line::width)
115 .max()
116 .unwrap_or(0)
117 };
118 let unbounded = Context::unbounded().at_tab_width(context.tab_width());
119 Measurement::new(widest(&context.at_width(1)), widest(&unbounded))
120 }
121
122 fn render(&self, context: &Context) -> Vec<Line> {
123 self.lines(context)
124 }
125}