gm-tui 0.0.1

TUI-based Ethereum wallet
Documentation
use crossterm::event::KeyCode;
use ratatui::{
    layout::{Constraint, Layout, Rect},
    widgets::Widget,
};

use crate::tui::{
    traits::{HandleResult, RectUtil},
    Event,
};

use super::scroll_bar::CustomScrollBar;

#[derive(Default)]
pub struct TextScroll {
    pub text: String,
    pub scroll_offset: usize,
}

impl TextScroll {
    pub fn new(text: String) -> Self {
        Self {
            text,
            scroll_offset: 0,
        }
    }

    fn lines(&self, width: usize) -> Vec<&str> {
        self.text
            .lines()
            .flat_map(|line| split_str_by_width(line, width))
            .collect()
    }

    pub fn scroll_up(&mut self) {
        if self.scroll_offset > 0 {
            self.scroll_offset -= 1;
        }
    }

    pub fn scroll_down(&mut self, width: usize, height: usize) {
        let lines = self.lines(width).len();
        if self.scroll_offset + height < lines {
            self.scroll_offset += 1;
        }
    }

    pub fn get_visible_text(&self, area: Rect) -> (Vec<&str>, usize) {
        let lines: Vec<&str> = self.lines(area.width as usize);
        (
            lines
                .iter()
                .skip(self.scroll_offset)
                .take(area.height as usize)
                .map(|line| line.trim_end())
                .collect(),
            lines.len(),
        )
    }

    pub fn handle_event(
        &mut self,
        event: &Event,
        area: ratatui::prelude::Rect,
    ) -> crate::Result<HandleResult> {
        #[allow(clippy::single_match)]
        match event {
            Event::Input(key) => match key.code {
                KeyCode::Up => {
                    self.scroll_up();
                }
                KeyCode::Down => {
                    self.scroll_down(area.width as usize, area.height as usize);
                }
                _ => {}
            },
            _ => {}
        }
        Ok(HandleResult::default())
    }
}

fn split_str_by_width(s: &str, width: usize) -> Vec<&str> {
    let mut result = Vec::new();
    let mut start = 0;
    let mut count = 0;

    for (i, _) in s.char_indices() {
        if count == width {
            result.push(&s[start..i]);
            start = i;
            count = 0;
        }
        count += 1;
    }

    if start < s.len() {
        result.push(&s[start..]);
    }

    result
}

impl Widget for &TextScroll {
    fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer)
    where
        Self: Sized,
    {
        let [mut text_area, scroll_area] =
            Layout::horizontal([Constraint::Min(1), Constraint::Length(1)]).areas(area);

        let (lines, total) = self.get_visible_text(text_area);
        if total > area.height as usize {
            for line in &lines {
                line.render(text_area, buf);
                let Ok(text_area_new) = text_area.consume_height(1) else {
                    return;
                };
                text_area = text_area_new;
            }

            CustomScrollBar {
                cursor: self.scroll_offset,
                total: total - area.height as usize,
            }
            .render(scroll_area, buf);
        } else {
            for line in &lines {
                line.render(text_area, buf);
                let Ok(text_area_new) = text_area.consume_height(1) else {
                    return;
                };
                text_area = text_area_new;
            }
        }
    }
}