termimad/
area.rs

1use {
2    crate::crossterm::terminal,
3    std::convert::{
4        TryFrom,
5        TryInto,
6    },
7};
8
9/// A default width which is used when we failed measuring the real terminal width
10const DEFAULT_TERMINAL_WIDTH: u16 = 50;
11
12/// A default height which is used when we failed measuring the real terminal width
13const DEFAULT_TERMINAL_HEIGHT: u16 = 20;
14
15/// A rectangular part of the screen
16#[derive(Debug, PartialEq, Eq, Clone)]
17pub struct Area {
18    pub left: u16,
19    pub top: u16,
20    pub width: u16,
21    pub height: u16,
22}
23
24impl Default for Area {
25    fn default() -> Self {
26        Self::uninitialized()
27    }
28}
29
30impl Area {
31    /// build a new area. You'll need to set the position and size
32    /// before you can use it
33    pub const fn uninitialized() -> Area {
34        Area {
35            left: 0,
36            top: 0,
37            height: 1,
38            width: 5,
39        }
40    }
41
42    /// build a new area.
43    pub const fn new(left: u16, top: u16, width: u16, height: u16) -> Area {
44        Area {
45            left,
46            top,
47            width,
48            height,
49        }
50    }
51
52    /// build an area covering the whole terminal
53    pub fn full_screen() -> Area {
54        let (width, height) = terminal_size();
55        Area {
56            left: 0,
57            top: 0,
58            width,
59            height,
60        }
61    }
62
63    pub const fn right(&self) -> u16 {
64        self.left + self.width
65    }
66
67    pub const fn bottom(&self) -> u16 {
68        self.top + self.height
69    }
70
71    /// tell whether the char at (x,y) is in the area
72    pub const fn contains(&self, x: u16, y: u16) -> bool {
73        x >= self.left && x < self.left + self.width && y >= self.top && y < self.top + self.height
74    }
75
76    /// shrink the area
77    pub fn pad(&mut self, dx: u16, dy: u16) {
78        // this will crash if padding is too big. feature?
79        self.left += dx;
80        self.top += dy;
81        self.width -= 2 * dx;
82        self.height -= 2 * dy;
83    }
84
85    /// symmetrically shrink the area if its width is bigger than `max_width`
86    pub fn pad_for_max_width(&mut self, max_width: u16) {
87        if max_width >= self.width {
88            return;
89        }
90        let pw = self.width - max_width;
91        self.left += pw / 2;
92        self.width -= pw;
93    }
94
95    /// Return an option which when filled contains
96    ///  a tupple with the top and bottom of the vertical
97    ///  scrollbar. Return none when the content fits
98    ///  the available space.
99    pub fn scrollbar<U>(
100        &self,
101        scroll: U, // number of lines hidden on top
102        content_height: U,
103    ) -> Option<(u16, u16)>
104    where
105        U: Into<usize>,
106    {
107        compute_scrollbar(scroll, content_height, self.height, self.top)
108    }
109}
110
111/// Compute the min and max y (from the top of the terminal, both inclusive)
112/// for the thumb part of the scrollbar which would represent the scrolled
113/// content in the available height.
114///
115/// If you represent some data in an Area, you should directly use the
116/// scrollbar method of Area.
117pub fn compute_scrollbar<U1, U2, U3>(
118    scroll: U1,           // 0 for no scroll, positive if scrolled
119    content_height: U1,   // number of lines of the content
120    available_height: U3, // for an area it's usually its height
121    top: U2,              // distance from the top of the screen
122) -> Option<(U2, U2)>
123where
124    U1: Into<usize>, // the type in which you store your content length and content scroll
125    U2: Into<usize> + TryFrom<usize>, // the drawing type (u16 for an area)
126    <U2 as TryFrom<usize>>::Error: std::fmt::Debug,
127    U3: Into<usize> + TryFrom<usize>, // the type used for available height
128    <U3 as TryFrom<usize>>::Error: std::fmt::Debug,
129{
130    let scroll: usize = scroll.into();
131    let content_height: usize = content_height.into();
132    let available_height: usize = available_height.into();
133    let top: usize = top.into();
134    if content_height <= available_height {
135        return None;
136    }
137    let mut track_before = scroll * available_height / content_height;
138    if track_before == 0 && scroll > 0 {
139        track_before = 1;
140    }
141    let thumb_height = available_height * available_height / content_height;
142    let scrollbar_top = top + track_before;
143    let mut scrollbar_bottom = scrollbar_top + thumb_height;
144    if scroll + available_height < content_height && available_height > 3 {
145        scrollbar_bottom = scrollbar_bottom
146            .min(top + available_height - 2)
147            .max(scrollbar_top);
148    }
149    // by construction those two conversions are OK
150    // (or it's a bug, which, well, is possible...)
151    let scrollbar_top = scrollbar_top.try_into().unwrap();
152    let scrollbar_bottom = scrollbar_bottom.try_into().unwrap();
153    Some((scrollbar_top, scrollbar_bottom))
154}
155
156/// Return a (width, height) with the dimensions of the available
157/// terminal in characters.
158#[must_use]
159pub fn terminal_size() -> (u16, u16) {
160    let size = terminal::size();
161    size.unwrap_or((DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_HEIGHT))
162}