Skip to main content

basalt_tui/
help_modal.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::{Alignment, Constraint, Flex, Layout, Rect, Size},
4    style::{Style, Stylize},
5    text::Line,
6    widgets::{
7        Block, BorderType, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
8        ScrollbarState, StatefulWidget, Widget, Wrap,
9    },
10};
11
12use crate::app::{calc_scroll_amount, Message as AppMessage, ScrollAmount};
13use crate::config::Theme;
14
15fn modal_area_height(size: Size) -> usize {
16    let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
17    let [area] = vertical.areas(Rect::new(0, 0, size.width, size.height.saturating_sub(3)));
18    area.height.into()
19}
20
21#[derive(Clone, Debug, PartialEq)]
22pub enum Message {
23    Toggle,
24    Close,
25    ScrollUp(ScrollAmount),
26    ScrollDown(ScrollAmount),
27}
28
29pub fn update<'a>(
30    message: &Message,
31    screen_size: Size,
32    state: &mut HelpModalState,
33) -> Option<AppMessage<'a>> {
34    match message {
35        Message::Toggle => state.toggle_visibility(),
36        Message::Close => state.hide(),
37        Message::ScrollDown(scroll_amount) => {
38            state.scroll_down(calc_scroll_amount(
39                scroll_amount,
40                modal_area_height(screen_size),
41            ));
42        }
43        Message::ScrollUp(scroll_amount) => {
44            state.scroll_up(calc_scroll_amount(
45                scroll_amount,
46                modal_area_height(screen_size),
47            ));
48        }
49    };
50
51    None
52}
53
54#[derive(Debug, Default, Clone, PartialEq)]
55pub struct HelpModalState {
56    pub scrollbar_state: ScrollbarState,
57    pub scrollbar_position: usize,
58    pub text: String,
59    pub visible: bool,
60}
61
62impl HelpModalState {
63    pub fn new(text: &str) -> Self {
64        Self {
65            text: text.to_string(),
66            scrollbar_state: ScrollbarState::new(text.lines().count()),
67            ..Default::default()
68        }
69    }
70
71    pub fn toggle_visibility(&mut self) {
72        self.visible = !self.visible;
73    }
74
75    pub fn hide(&mut self) {
76        self.visible = false;
77    }
78
79    pub fn scroll_up(&mut self, amount: usize) {
80        let scrollbar_position = self.scrollbar_position.saturating_sub(amount);
81        let scrollbar_state = self.scrollbar_state.position(scrollbar_position);
82
83        self.scrollbar_state = scrollbar_state;
84        self.scrollbar_position = scrollbar_position;
85    }
86
87    pub fn scroll_down(&mut self, amount: usize) {
88        let scrollbar_position = self
89            .scrollbar_position
90            .saturating_add(amount)
91            .min(self.text.lines().count());
92
93        let scrollbar_state = self.scrollbar_state.position(scrollbar_position);
94
95        self.scrollbar_state = scrollbar_state;
96        self.scrollbar_position = scrollbar_position;
97    }
98}
99
100fn modal_area(area: Rect) -> Rect {
101    let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
102    let horizontal = Layout::horizontal([Constraint::Length(83)]).flex(Flex::Center);
103    let [area] = vertical.areas(area);
104    let [area] = horizontal.areas(area);
105    area
106}
107
108pub struct HelpModal {
109    pub border_type: BorderType,
110    pub theme: Theme,
111}
112
113impl HelpModal {
114    pub fn new(border_type: BorderType, theme: Theme) -> Self {
115        Self { border_type, theme }
116    }
117}
118
119impl StatefulWidget for HelpModal {
120    type State = HelpModalState;
121
122    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
123    where
124        Self: Sized,
125    {
126        let block = Block::bordered()
127            .fg(self.theme.muted)
128            .bg(self.theme.background)
129            .border_type(self.border_type)
130            .padding(Padding::uniform(1))
131            .title_style(Style::default().italic().bold())
132            .title(" Help ")
133            .title(Line::from(" (?) ").alignment(Alignment::Right));
134
135        let area = modal_area(area);
136
137        Widget::render(Clear, area, buf);
138        Widget::render(
139            Paragraph::new(state.text.clone())
140                .wrap(Wrap::default())
141                .scroll((state.scrollbar_position as u16, 0))
142                .block(block)
143                .fg(self.theme.text),
144            area,
145            buf,
146        );
147
148        StatefulWidget::render(
149            Scrollbar::new(ScrollbarOrientation::VerticalRight),
150            area,
151            buf,
152            &mut state.scrollbar_state,
153        );
154    }
155}