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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
use {
    crossterm::terminal,
    std::convert::{TryFrom, TryInto},
};

/// 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,
}

impl Default for Area {
    fn default() -> Self {
        Self::uninitialized()
    }
}

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

    /// build a new area.
    pub const 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,
        }
    }

    pub const fn right(&self) -> u16 {
        self.left + self.width
    }

    pub const fn bottom(&self) -> u16 {
        self.top + self.height
    }

    /// tell whether the char at (x,y) is in the area
    pub const 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<U>(
        &self,
        scroll: U, // number of lines hidden on top
        content_height: U,
    ) -> Option<(u16, u16)>
    where U: Into<usize>
    {
        compute_scrollbar(scroll, content_height, self.height, self.top)
    }
}

/// Compute the min and max y (from the top of the terminal, both inclusive)
/// for the thumb part of the scrollbar which would represent the scrolled
/// content in the available height.
///
/// If you represent some data in an Area, you should directly use the
/// scrollbar method of Area.
pub fn compute_scrollbar<U1, U2, U3>(
    scroll: U1,           // 0 for no scroll, positive if scrolled
    content_height: U1,   // number of lines of the content
    available_height: U3, // for an area it's usually its height
    top: U2,              // distance from the top of the screen
) -> Option<(U2, U2)>
where
    U1: Into<usize>, // the type in which you store your content length and content scroll
    U2: Into<usize> + TryFrom<usize>, // the drawing type (u16 for an area)
    <U2 as TryFrom<usize>>::Error: std::fmt::Debug,
    U3: Into<usize> + TryFrom<usize>, // the type used for available height
    <U3 as TryFrom<usize>>::Error: std::fmt::Debug,
{
    let scroll: usize = scroll.into();
    let content_height: usize = content_height.into();
    let available_height: usize = available_height.into();
    let top: usize = top.into();
    if content_height <= available_height {
        return None;
    }
    let mut track_before = scroll * available_height / content_height;
    if track_before == 0 && scroll > 0 {
        track_before = 1;
    }
    let thumb_height = available_height * available_height / content_height;
    let scrollbar_top = top + track_before;
    let mut scrollbar_bottom = scrollbar_top + thumb_height;
    if scroll + available_height < content_height && available_height > 3 {
        scrollbar_bottom = scrollbar_bottom
            .min(top + available_height - 2)
            .max(scrollbar_top);
    }
    // by construction those two conversions are OK
    // (or it's a bug, which, well, is possible...)
    let scrollbar_top = scrollbar_top.try_into().unwrap();
    let scrollbar_bottom = scrollbar_bottom.try_into().unwrap();
    Some((scrollbar_top, scrollbar_bottom))
}

/// 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))
}