r3bl_tuify 0.3.0

DEPRECATED: Use r3bl_tui instead. Lightweight TUI capabilities for CLI apps
/*
 *   Copyright (c) 2023 R3BL LLC
 *   All rights reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */

//! ### Vertical scrolling and viewport
//!
//! When you call [locate_cursor_in_viewport] function, it returns a location that is
//! relative to the viewport. The viewport is the visible area of the terminal.
//!
//! ```text
//!                    +0--------------------+
//!                    0                     |
//!                    |        above        | <- caret_row_scroll_adj
//!                    |                     |
//!                    +--- scroll_offset ---+
//!              ->    |         ↑           |      ↑
//!   raw_caret  |     |                     |      |
//!   _row_index |     |      within vp      |  vp height
//!              |     |                     |      |
//!              ->    |         ↓           |      ↓
//!                    +--- scroll_offset ---+
//!                    |    + vp height      |
//!                    |                     |
//!                    |        below        | <- caret_row_scroll_adj
//!                    |                     |
//!                    +---------------------+
//! ```
//!
//! What the [CaretVerticalViewportLocation] enum represents:
//!
//! ```text
//!    +0--------------------+ <- AtAbsoluteTop
//!    0                     |
//!    |        above        | <- AboveTopOfViewport
//!    |                     |
//!    +--- scroll_offset ---+ <- AtTopOfViewport
//!    |         ↑           |
//!    |                     |
//!    |      within vp      | <- InMiddleOfViewport
//!    |                     |
//!    |         ↓           |
//!    +--- scroll_offset ---+ <- AtBottomOfViewport
//!    |    + vp height      |
//!    |                     |
//!    |        below        | <- BelowBottomOfViewport
//!    |                     |
//!    +---------------------+ <- AtAbsoluteBottom
//! ```

use crossterm::style::Stylize;
use r3bl_core::{call_if_true, ch, ChUnit};

use crate::DEVELOPMENT_MODE;

#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum CaretVerticalViewportLocation {
    AtAbsoluteTop,
    AboveTopOfViewport,
    AtTopOfViewport,
    InMiddleOfViewport,
    AtBottomOfViewport,
    BelowBottomOfViewport,
    AtAbsoluteBottom,
    NotFound,
}

pub fn get_scroll_adjusted_row_index(
    raw_caret_row_index: ChUnit,
    scroll_offset_row_index: ChUnit,
) -> ChUnit {
    raw_caret_row_index + scroll_offset_row_index
}

pub fn locate_cursor_in_viewport(
    raw_caret_row_index: ChUnit,
    scroll_offset_row_index: ChUnit,
    display_height: ChUnit,
    items_size: ChUnit,
) -> CaretVerticalViewportLocation {
    let abs_row_index =
        get_scroll_adjusted_row_index(raw_caret_row_index, scroll_offset_row_index);

    call_if_true!(DEVELOPMENT_MODE, {
        tracing::debug!(
            "locate_cursor_in_viewport(): raw_caret_row_index: {}, scroll_offset_row_index: {}, abs_row_index: {}, display_height: {}, items_size: {}",
            format!("{raw_caret_row_index}").green(),
            format!("{scroll_offset_row_index}").green(),
            format!("{abs_row_index}").green(),
            format!("{display_height}").green(),
            format!("{items_size}").green()
        );
    });

    // Note the ordering of the statements below matters.
    if abs_row_index == items_size - 1 {
        // AtAbsoluteBottom takes precedence over AtAbsoluteTop when there is only one item.
        CaretVerticalViewportLocation::AtAbsoluteBottom
    } else if abs_row_index == ch!(0) {
        CaretVerticalViewportLocation::AtAbsoluteTop
    } else if abs_row_index < scroll_offset_row_index {
        CaretVerticalViewportLocation::AboveTopOfViewport
    } else if abs_row_index == scroll_offset_row_index {
        CaretVerticalViewportLocation::AtTopOfViewport
    }
    // When comparing height or width or size to index, we need to subtract 1.
    else if abs_row_index > scroll_offset_row_index
        && abs_row_index < (scroll_offset_row_index + display_height - 1)
    {
        CaretVerticalViewportLocation::InMiddleOfViewport
    } else if abs_row_index == (scroll_offset_row_index + display_height - 1) {
        CaretVerticalViewportLocation::AtBottomOfViewport
    } else if abs_row_index > (scroll_offset_row_index + display_height - 1) {
        CaretVerticalViewportLocation::BelowBottomOfViewport
    } else {
        CaretVerticalViewportLocation::NotFound
    }
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_get_scroll_adjusted_row_index() {
        assert_eq!(get_scroll_adjusted_row_index(ch!(0), ch!(0)), ch!(0));
        assert_eq!(get_scroll_adjusted_row_index(ch!(1), ch!(0)), ch!(1));
        assert_eq!(get_scroll_adjusted_row_index(ch!(0), ch!(1)), ch!(1));
        assert_eq!(get_scroll_adjusted_row_index(ch!(2), ch!(3)), ch!(5));
    }

    #[test]
    fn test_locate_cursor_in_viewport() {
        assert_eq!(
            locate_cursor_in_viewport(ch!(0), ch!(0), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::AtAbsoluteTop
        );
        assert_eq!(
            locate_cursor_in_viewport(ch!(19), ch!(0), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::AtAbsoluteBottom
        );
        assert_eq!(
            locate_cursor_in_viewport(ch!(5), ch!(0), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::InMiddleOfViewport
        );
        assert_eq!(
            locate_cursor_in_viewport(ch!(0), ch!(5), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::AtTopOfViewport
        );
        assert_eq!(
            locate_cursor_in_viewport(ch!(5), ch!(5), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::InMiddleOfViewport
        );
        assert_eq!(
            locate_cursor_in_viewport(ch!(14), ch!(5), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::AtAbsoluteBottom
        );
        assert_eq!(
            locate_cursor_in_viewport(ch!(15), ch!(5), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::BelowBottomOfViewport
        );
        assert_eq!(
            locate_cursor_in_viewport(ch!(21), ch!(5), ch!(10), ch!(20)),
            CaretVerticalViewportLocation::BelowBottomOfViewport
        );
    }
}