1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use crossterm::terminal;

/// A default width which is used when we failed measuring the real terminal width
const DEFAULT_TERMINAL_WIDTH: u16 = 50;

/// A default height which is used when we failed measuring the real terminal width
const DEFAULT_TERMINAL_HEIGHT: u16 = 20;

pub trait AreaContent {
    fn height() -> u16;
}

/// A rectangular part of the screen
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Area {
    pub left: u16,
    pub top: u16,
    pub width: u16,
    pub height: u16,
}

fn div_ceil(a: i32, b: i32) -> i32 {
    a / b + if a % b != 0 { 1 } else { 0 }
}

impl Area {
    /// build a new area. You'll need to set the position and size
    /// before you can use it
    pub fn uninitialized() -> Area {
        Area {
            left: 0,
            top: 0,
            height: 1,
            width: 5,
        }
    }

    /// build a new area.
    pub fn new(left: u16, top: u16, width: u16, height: u16) -> Area {
        Area {
            left,
            top,
            width,
            height,
        }
    }

    /// build an area covering the whole terminal
    pub fn full_screen() -> Area {
        let (width, height) = terminal_size();
        Area {
            left: 0,
            top: 0,
            width,
            height,
        }
    }

    /// tell whether the char at (x,y) is in the area
    pub fn contains(&self, x: u16, y: u16) -> bool {
        x >= self.left
            && x < self.left + self.width
            && y >= self.top
            && y < self.top + self.height
    }

    /// shrink the area
    pub fn pad(&mut self, dx: u16, dy: u16) {
        // this will crash if padding is too big. feature?
        self.left += dx;
        self.top += dy;
        self.width -= 2 * dx;
        self.height -= 2 * dy;
    }

    /// symmetrically shrink the area if its width is bigger than `max_width`
    pub fn pad_for_max_width(&mut self, max_width: u16) {
        if max_width >= self.width {
            return;
        }
        let pw = self.width - max_width;
        self.left += pw / 2;
        self.width -= pw;
    }

    /// Return an option which when filled contains
    ///  a tupple with the top and bottom of the vertical
    ///  scrollbar. Return none when the content fits
    ///  the available space.
    pub fn scrollbar(
        &self,
        scroll: i32, // 0 for no scroll, positive if scrolled
        content_height: i32,
    ) -> Option<(u16, u16)> {
        compute_scrollbar(scroll, content_height, i32::from(self.height))
    }
}

pub fn compute_scrollbar(
    scroll: i32,           // 0 for no scroll, positive if scrolled
    content_height: i32,   // number of lines of the content
    available_height: i32, // for an area it's usually its height
) -> Option<(u16, u16)> {
    let h = available_height;
    if content_height <= h {
        return None;
    }
    let sc = div_ceil(scroll * h, content_height);
    let hidden_tail = content_height - scroll - h;
    let se = div_ceil(hidden_tail * h, content_height);
    Some((
        sc as u16,
        if h > sc + se {
            (h - se) as u16
        } else {
            sc as u16 + 1
        },
    ))
}

/// Return a (width, height) with the dimensions of the available
/// terminal in characters.
///
pub fn terminal_size() -> (u16, u16) {
    let size = terminal::size();
    size.unwrap_or((DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_HEIGHT))
}