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
128
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), self.top)
    }
}

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
    top: u16,
) -> 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 {
            top + (h - se) as u16
        } else {
            top + 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))
}