Skip to main content

ctl_layout/
definitions.rs

1//! Entries drawn as labels over an indented description.
2
3use std::num::NonZeroU16;
4
5use ctl_text::{Context, Line, Measurement, Overflow, Renderable, Text};
6
7use crate::lines::{indent_lines, literal};
8
9/// The indent of an entry's label line.
10const LABEL_INDENT: u16 = 2;
11/// The indent of an entry's description lines.
12const DESCRIPTION_INDENT: u16 = 4;
13
14/// Entries, each a line of labels over indented lines of description.
15///
16/// An entry with two labels and one description line draws as:
17///
18/// ```text
19///   -f --format
20///     Output representation
21/// ```
22///
23/// It is the form of a table of flags or commands for a narrow width, where
24/// the columns no longer fit side by side. Put it in a
25/// [`Breakpoint`](crate::Breakpoint) beside the table.
26///
27/// The labels of an entry share one line, separated by a space. Its
28/// description lines go below, wrapped at spaces to the width that is left.
29/// An entry whose labels or description are empty skips that part.
30#[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    /// A list with no entries.
43    #[must_use]
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Adds an entry: its `labels` on a line indented by 2, and its
49    /// `description` below them, indented by 4. Each label and description
50    /// line converts to a [`Text`], and one with no text adds nothing.
51    #[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    /// Every entry laid out in `context`.
76    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
93/// The non-empty lines of `cells` on one line, separated by a space.
94fn 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}