Skip to main content

gitv_tui/ui/components/
help.rs

1use ratatui::{
2    style::{Color, Modifier, Style},
3    text::{Line, Span, Text},
4    widgets::{BlockExt, Clear, Widget},
5};
6use tracing::trace;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum HelpElementKind {
10    Keybind(&'static str, &'static str),
11    Text(&'static str),
12}
13
14#[macro_export]
15macro_rules! help_keybind {
16    ($key:expr, $description:expr) => {
17        $crate::ui::components::help::HelpElementKind::Keybind($key, $description)
18    };
19}
20
21#[macro_export]
22macro_rules! help_text {
23    ($text:expr) => {
24        $crate::ui::components::help::HelpElementKind::Text($text)
25    };
26}
27
28pub fn help_elements_to_text(elements: &[HelpElementKind], width: u16) -> Text<'static> {
29    let mut lines = Vec::with_capacity(elements.len());
30    for element in elements {
31        match element {
32            HelpElementKind::Keybind(key, description) => {
33                let total_length = (key.len() + description.len()) as u16; // +1 for the space between
34                let padding = if total_length < width {
35                    width - total_length
36                } else {
37                    1 // Ensure at least one space if the content exceeds the width
38                };
39                lines.push(Line::from(vec![
40                    Span::styled(
41                        *key,
42                        Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
43                    ),
44                    Span::raw(" ".repeat(padding as usize)),
45                    Span::raw(*description),
46                ]));
47            }
48            HelpElementKind::Text(text) => {
49                let wrapped = textwrap::wrap(text, width as usize);
50                lines.extend(wrapped.into_iter().map(|line| Line::from(line).centered()));
51            }
52        }
53    }
54    Text::from(lines)
55}
56
57/// A simple component to display help information. It can be centered within its parent area using the `set_constraints` method.
58pub struct HelpComponent<'a> {
59    constraint: u16,
60    content: &'a [HelpElementKind],
61    block: Option<ratatui::widgets::Block<'a>>,
62    width: u16,
63}
64
65impl<'a> HelpComponent<'a> {
66    /// Creates a new HelpComponent with the given content.
67    pub fn new(content: &'a [HelpElementKind]) -> Self {
68        Self {
69            content,
70            width: 0,
71            constraint: 0,
72            block: None,
73        }
74    }
75    /// Sets the constraints for centering the component. The constraints are specified as percentages of the parent area.
76    pub fn set_constraint(self, constraint: u16) -> Self {
77        Self { constraint, ..self }
78    }
79    /// Sets a block around the component. This can be used to visually separate the help content from other UI elements.
80    pub fn block(self, block: ratatui::widgets::Block<'a>) -> Self {
81        Self {
82            block: Some(block),
83            ..self
84        }
85    }
86}
87
88impl<'a> Widget for HelpComponent<'a> {
89    fn render(mut self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
90        use ratatui::layout::Constraint::{Length, Percentage};
91        trace!(content = ?self.content, "Rendering HelpComponent");
92        trace!(content_length = ?self.content.len(), "Content length");
93        let mut centered_area = if self.constraint != 0 {
94            area.centered(Percentage(self.constraint), Length(self.constraint))
95        } else {
96            area
97        };
98        let mut inner = self.block.inner_if_some(centered_area);
99        self.width = inner.width;
100        let text = help_elements_to_text(self.content, self.width);
101        let text_height = text.height() as u16;
102        let y_offset = |h: u16| {
103            if text_height < h {
104                (h - text_height) / 2
105            } else {
106                0
107            }
108        };
109        inner.y += y_offset(inner.height) + 1;
110        inner.height = text.height() as u16;
111        let inner_height = inner.height;
112        centered_area.y += y_offset(centered_area.height);
113        centered_area.height = inner_height + 2;
114        Clear.render(centered_area, buf);
115        self.block.render(centered_area, buf);
116        text.render(inner, buf);
117    }
118}