basalt_tui/
help_modal.rs

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