basalt-tui 0.12.4

Basalt TUI application for Obsidian notes.
Documentation
use ratatui::{
    buffer::Buffer,
    layout::{Alignment, Constraint, Flex, Layout, Rect, Size},
    style::{Color, Style, Stylize},
    text::Line,
    widgets::{
        Block, BorderType, Clear, Padding, Paragraph, Scrollbar, ScrollbarOrientation,
        ScrollbarState, StatefulWidget, Widget, Wrap,
    },
};

use crate::app::{calc_scroll_amount, Message as AppMessage, ScrollAmount};

fn modal_area_height(size: Size) -> usize {
    let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
    let [area] = vertical.areas(Rect::new(0, 0, size.width, size.height.saturating_sub(3)));
    area.height.into()
}

#[derive(Clone, Debug, PartialEq)]
pub enum Message {
    Toggle,
    Close,
    ScrollUp(ScrollAmount),
    ScrollDown(ScrollAmount),
}

pub fn update<'a>(
    message: &Message,
    screen_size: Size,
    state: &mut HelpModalState,
) -> Option<AppMessage<'a>> {
    match message {
        Message::Toggle => state.toggle_visibility(),
        Message::Close => state.hide(),
        Message::ScrollDown(scroll_amount) => {
            state.scroll_down(calc_scroll_amount(
                scroll_amount,
                modal_area_height(screen_size),
            ));
        }
        Message::ScrollUp(scroll_amount) => {
            state.scroll_up(calc_scroll_amount(
                scroll_amount,
                modal_area_height(screen_size),
            ));
        }
    };

    None
}

#[derive(Debug, Default, Clone, PartialEq)]
pub struct HelpModalState {
    pub scrollbar_state: ScrollbarState,
    pub scrollbar_position: usize,
    pub text: String,
    pub visible: bool,
}

impl HelpModalState {
    pub fn new(text: &str) -> Self {
        Self {
            text: text.to_string(),
            scrollbar_state: ScrollbarState::new(text.lines().count()),
            ..Default::default()
        }
    }

    pub fn toggle_visibility(&mut self) {
        self.visible = !self.visible;
    }

    pub fn hide(&mut self) {
        self.visible = false;
    }

    pub fn scroll_up(&mut self, amount: usize) {
        let scrollbar_position = self.scrollbar_position.saturating_sub(amount);
        let scrollbar_state = self.scrollbar_state.position(scrollbar_position);

        self.scrollbar_state = scrollbar_state;
        self.scrollbar_position = scrollbar_position;
    }

    pub fn scroll_down(&mut self, amount: usize) {
        let scrollbar_position = self
            .scrollbar_position
            .saturating_add(amount)
            .min(self.text.lines().count());

        let scrollbar_state = self.scrollbar_state.position(scrollbar_position);

        self.scrollbar_state = scrollbar_state;
        self.scrollbar_position = scrollbar_position;
    }
}

fn modal_area(area: Rect) -> Rect {
    let vertical = Layout::vertical([Constraint::Percentage(50)]).flex(Flex::Center);
    let horizontal = Layout::horizontal([Constraint::Length(83)]).flex(Flex::Center);
    let [area] = vertical.areas(area);
    let [area] = horizontal.areas(area);
    area
}

pub struct HelpModal {
    pub border_type: BorderType,
}

impl HelpModal {
    pub fn new(border_type: BorderType) -> Self {
        Self { border_type }
    }
}

impl StatefulWidget for HelpModal {
    type State = HelpModalState;

    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
    where
        Self: Sized,
    {
        let block = Block::bordered()
            .dark_gray()
            .border_type(self.border_type)
            .padding(Padding::uniform(1))
            .title_style(Style::default().italic().bold())
            .title(" Help ")
            .title(Line::from(" (?) ").alignment(Alignment::Right));

        let area = modal_area(area);

        Widget::render(Clear, area, buf);
        Widget::render(
            Paragraph::new(state.text.clone())
                .wrap(Wrap::default())
                .scroll((state.scrollbar_position as u16, 0))
                .block(block)
                .fg(Color::default()),
            area,
            buf,
        );

        StatefulWidget::render(
            Scrollbar::new(ScrollbarOrientation::VerticalRight),
            area,
            buf,
            &mut state.scrollbar_state,
        );
    }
}