termint 0.8.1

Library for colored printing and Terminal User Interfaces
Documentation
use std::{
    cell::Cell,
    hash::{DefaultHasher, Hash, Hasher},
    marker::PhantomData,
    rc::Rc,
};

use crate::{
    buffer::Buffer,
    geometry::Vec2Range,
    prelude::{Direction, Rect, Vec2},
    style::Style,
    widgets::{Element, LayoutNode, Widget},
};

/// A scrollbar widget that can be either vertical or horizontal.
///
/// A [`Scrollbar`] is typically composed into other widgets (like
/// [`Scrollable`](crate::widgets::Scrollable)) to visualize their current view
/// state.
///
/// It relias on a shared [`ScrollbarState`] to determine the position and size
/// of the thumb. This state is usually udpated by the parent widget during
/// the render phase, when the content length becomes known.
///
/// # Terminology
///
/// - Thumb = the moving part representing the current visible viewport.
/// - Track = the background area representing teh full length of the content.
///
/// # Example:
/// ```rust
/// use termint::{prelude::*, widgets::{Scrollbar, ScrollbarState}};
/// use std::{cell::Cell, rc::Rc};
///
/// // Scrollbar state with fixed content length and offset
/// let state = Rc::new(Cell::new(ScrollbarState::new(15).content_len(30)));
///
/// // Creates new horizontal scrollbar with the shared state
/// // This will create scrollbar similar to: `---=---`.
/// let scrollbar = Scrollbar::<()>::horizontal(state.clone())
///     .track_char('-')
///     .thumb_char('=');
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct Scrollbar<M: 'static = ()> {
    track_char: char,
    track_style: Style,
    thumb_char: char,
    thumb_style: Style,
    direction: Direction,
    state: Rc<Cell<ScrollbarState>>,
    _marker: PhantomData<M>,
}

/// Tracks the scroll position and content size.
///
/// In the event handling of the app, you can handle key events and change the
/// scroll offset
///
/// # Example
/// ```rust
/// # use termint::widgets::ScrollbarState;
/// let mut state = ScrollbarState::new(0).content_len(50);
///
/// state = state.next();
/// assert_eq!(state.offset, 1);
///
/// state = state.advance(5);
/// assert_eq!(state.offset, 6);
///
/// state = state.prev();
/// assert_eq!(state.offset, 5);
///
/// state = state.retreat(3);
/// assert_eq!(state.offset, 2);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct ScrollbarState {
    pub content_len: usize,
    pub offset: usize,
}

impl<M> Scrollbar<M> {
    /// Creates a vertical [`Scrollbar`] with the given state.
    ///
    /// Uses `│` character for track and `┃` for thumb by default.
    ///
    /// # Example
    /// ```rust
    /// use termint::widgets::Scrollbar;
    /// use std::{cell::Cell, rc::Rc};
    ///
    /// let scrollbar = Scrollbar::<()>::vertical(Default::default());
    /// ```
    #[must_use]
    pub fn vertical(state: Rc<Cell<ScrollbarState>>) -> Self {
        Self {
            state,
            ..Default::default()
        }
    }

    /// Creates a horizontal [`Scrollbar`] with the given state.
    ///
    /// Uses `─` character for track and `━` for thumb by default.
    ///
    /// # Example
    /// ```rust
    /// use termint::widgets::Scrollbar;
    /// use std::{cell::Cell, rc::Rc};
    ///
    /// let scrollbar = Scrollbar::<()>::horizontal(Default::default());
    /// ```
    #[must_use]
    pub fn horizontal(state: Rc<Cell<ScrollbarState>>) -> Self {
        Self {
            direction: Direction::Horizontal,
            track_char: '',
            thumb_char: '',
            state,
            ..Default::default()
        }
    }

    /// Sets the character used to draw the [`Scrollbar`] track.
    #[must_use]
    pub fn track_char(mut self, track_char: char) -> Self {
        self.track_char = track_char;
        self
    }

    /// Sets the style of the [`Scrollbar`] track.
    ///
    /// The `style` can be any type convertible to [`Style`].
    #[must_use]
    pub fn track_style<T>(mut self, style: T) -> Self
    where
        T: Into<Style>,
    {
        self.track_style = style.into();
        self
    }

    /// Sets the character used to draw the [`Scrollbar`] thumb (the moving
    /// part).
    #[must_use]
    pub fn thumb_char(mut self, thumb_char: char) -> Self {
        self.thumb_char = thumb_char;
        self
    }

    /// Sets the style of the [`Scrollbar`] thumb.
    ///
    /// The `style` can be any type convertible to [`Style`].
    #[must_use]
    pub fn thumb_style<T>(mut self, style: T) -> Self
    where
        T: Into<Style>,
    {
        self.thumb_style = style.into();
        self
    }

    /// Sets the [`Direction`] of the [`Scrollbar`].
    #[must_use]
    pub fn direction(mut self, direction: Direction) -> Self {
        self.direction = direction;
        self
    }

    /// Sets the scroll offset in the [`ScrollbarState`].
    pub fn offset(&self, offset: usize) {
        self.state.set(self.state.get().offset(offset));
    }

    /// Sets the total content length in the [`ScrollbarState`].
    pub fn content_len(&self, content_len: usize) {
        self.state.set(self.state.get().content_len(content_len));
    }

    /// Returns a copy of the current [`ScrollbarState`].
    pub fn get_state(&self) -> ScrollbarState {
        self.state.get()
    }
}

impl ScrollbarState {
    /// Creates a new [`ScrollbarState`] with the given initial offset.
    ///
    /// The content length defaults to zero.
    #[must_use]
    pub fn new(offset: usize) -> Self {
        Self {
            content_len: 0,
            offset,
        }
    }

    /// Sets the scroll offset.
    #[must_use]
    pub fn offset(mut self, offset: usize) -> Self {
        self.offset = offset;
        self
    }

    /// Sets the total content length.
    #[must_use]
    pub fn content_len(mut self, content_len: usize) -> Self {
        self.content_len = content_len;
        self
    }

    /// Increments the scroll offset by one, up to the end of the content.
    pub fn next(self) -> Self {
        self.advance(1)
    }

    /// Increments the scroll offset by the given number, up to the end of the
    /// content.
    pub fn advance(mut self, n: usize) -> Self {
        self.offset =
            (self.offset + n).min(self.content_len.saturating_sub(1));
        self
    }

    /// Decrements the scroll offset by one, down to zero.
    pub fn prev(self) -> Self {
        self.retreat(1)
    }

    /// Decrements the scroll offset by the given number, down to zero.
    pub fn retreat(mut self, n: usize) -> Self {
        self.offset = self.offset.saturating_sub(n);
        self
    }

    /// Resets the scroll offset to the start (zero).
    pub fn first(mut self) -> Self {
        self.offset = 0;
        self
    }

    /// Sets the scroll offset to the last valid position.
    pub fn last(mut self) -> Self {
        self.offset = self.content_len.saturating_sub(1);
        self
    }
}

impl<M: Clone + 'static> Widget<M> for Scrollbar<M> {
    fn render(&self, buffer: &mut Buffer, layout: &LayoutNode) {
        let rect = layout.area;
        match self.direction {
            Direction::Vertical => self.ver_render(buffer, &rect),
            Direction::Horizontal => self.hor_render(buffer, &rect),
        }
    }

    fn height(&self, size: &Vec2) -> usize {
        let total = self.state.get().content_len;
        match self.direction {
            Direction::Vertical => size.y,
            Direction::Horizontal => (total > size.y) as usize,
        }
    }

    fn width(&self, size: &Vec2) -> usize {
        let total = self.state.get().content_len;
        match self.direction {
            Direction::Vertical => (total > size.x) as usize,
            Direction::Horizontal => size.x,
        }
    }

    fn layout_hash(&self) -> u64 {
        let mut hasher = DefaultHasher::new();
        self.direction.hash(&mut hasher);
        hasher.finish()
    }
}

impl<M> Scrollbar<M> {
    /// Renders the vertical scrollbar
    fn ver_render(&self, buffer: &mut Buffer, rect: &Rect) {
        let Some((size, pos)) = self.calc_thumb(rect.height()) else {
            return;
        };

        self.render_track(
            buffer,
            rect.pos().to(Vec2::new(rect.x() + 1, rect.bottom() + 1)),
        );

        let start = Vec2::new(rect.x(), rect.y() + pos);
        let end = Vec2::new(rect.x() + 1, rect.y() + pos + size);
        self.render_thumb(buffer, start.to(end));
    }

    /// Renders the horizontal scrollbar
    fn hor_render(&self, buffer: &mut Buffer, rect: &Rect) {
        let Some((size, pos)) = self.calc_thumb(rect.width()) else {
            return;
        };

        self.render_track(
            buffer,
            rect.pos().to(Vec2::new(rect.right() + 1, rect.y() + 1)),
        );

        let start = Vec2::new(rect.x() + pos, rect.y());
        let end = Vec2::new(rect.x() + pos + size, rect.y() + 1);
        self.render_thumb(buffer, start.to(end));
    }

    /// Gets size of the thumb and its position
    fn calc_thumb(&self, visible: usize) -> Option<(usize, usize)> {
        let total = self.state.get().content_len;
        if total <= visible || visible == 0 {
            self.state.set(self.state.get().offset(0));
            return None;
        }

        let mut thumb_size =
            ((visible * visible) as f64 / total as f64).round() as usize;
        thumb_size = thumb_size.max(1);
        let max_offset = total - visible;

        let mut state = self.state.get();
        if state.offset > max_offset {
            state = state.offset(max_offset);
            self.state.set(state);
        }

        let pos = (state.offset as f64 / max_offset as f64
            * (visible - thumb_size) as f64)
            .round() as usize;

        Some((thumb_size, pos))
    }

    /// Renders the scrollbar track
    fn render_track(&self, buffer: &mut Buffer, pos_range: Vec2Range) {
        for pos in pos_range {
            buffer[pos].char(self.track_char).style(self.track_style);
        }
    }

    /// Renders the scrollbar thumb
    fn render_thumb(&self, buffer: &mut Buffer, pos_range: Vec2Range) {
        for pos in pos_range {
            buffer[pos].char(self.thumb_char).style(self.thumb_style);
        }
    }
}

impl<M> Default for Scrollbar<M> {
    fn default() -> Self {
        Self {
            track_char: '',
            track_style: Default::default(),
            thumb_char: '',
            thumb_style: Default::default(),
            direction: Default::default(),
            state: Default::default(),
            _marker: PhantomData,
        }
    }
}

impl<M: Clone + 'static> From<Scrollbar<M>> for Box<dyn Widget<M>> {
    fn from(value: Scrollbar<M>) -> Self {
        Box::new(value)
    }
}

impl<M: Clone + 'static> From<Scrollbar<M>> for Element<M> {
    fn from(value: Scrollbar<M>) -> Self {
        Element::new(value)
    }
}