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
//! Layout helpers.
use ratatui::layout::Rect;
/// Returns a `width` x `height` rect centered within `area`, clamped to it.
pub fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
let width = width.min(area.width);
let height = height.min(area.height);
Rect {
x: area.x + (area.width - width) / 2,
y: area.y + (area.height - height) / 2,
width,
height,
}
}
/// Grows `wanted` to at least `min`, then caps it at `max`.
///
/// Not `wanted.clamp(min, max)`: a terminal smaller than a widget's preferred
/// minimum makes `max < min`, and [`Ord::clamp`] panics on that. Here `max`
/// wins instead, because the available space is the hard limit and the minimum
/// is only a preference.
pub fn fit(wanted: u16, min: u16, max: u16) -> u16 {
wanted.max(min).min(max)
}
/// Returns a rect centered in `area` sized to the `numerator/denominator`
/// fraction of it, but never smaller than `min_width` x `min_height` (and never
/// larger than `area`). The shared sizing for centered popups.
///
/// A `denominator` of zero is treated as "the whole area" rather than dividing
/// by zero; it can only come from a caller mistake, and a popup sized to the
/// full area is the harmless reading.
pub fn centered_fraction(
area: Rect,
numerator: u16,
denominator: u16,
min_width: u16,
min_height: u16,
) -> Rect {
let width = fit(
scale(area.width, numerator, denominator),
min_width,
area.width,
);
let height = fit(
scale(area.height, numerator, denominator),
min_height,
area.height,
);
centered_rect(width, height, area)
}
/// Scales `extent` by `numerator/denominator`, in `u32` so the product cannot
/// overflow `u16` for a very wide area (`u16::MAX * 9` does not fit).
fn scale(extent: u16, numerator: u16, denominator: u16) -> u16 {
if denominator == 0 {
return extent;
}
let scaled =
u32::from(extent) * u32::from(numerator) / u32::from(denominator);
u16::try_from(scaled).unwrap_or(u16::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn centers_within_the_area() {
let area = Rect::new(0, 0, 100, 40);
let rect = centered_rect(40, 10, area);
assert_eq!(rect, Rect::new(30, 15, 40, 10));
}
#[test]
fn clamps_to_the_area_when_larger() {
let area = Rect::new(4, 6, 20, 8);
let rect = centered_rect(200, 200, area);
// Clamped to the area's size and pinned to its origin (no overflow).
assert_eq!(rect, Rect::new(4, 6, 20, 8));
}
#[test]
fn fit_prefers_the_minimum_but_the_maximum_wins() {
assert_eq!(fit(10, 5, 20), 10); // the wanted size fits
assert_eq!(fit(2, 5, 20), 5); // grown to the minimum
assert_eq!(fit(30, 5, 20), 20); // capped at the maximum
// The case `clamp` would panic on: no room for the minimum.
assert_eq!(fit(10, 28, 20), 20);
}
#[test]
fn centered_fraction_survives_an_area_below_its_minimum() {
let area = Rect::new(0, 0, 10, 3);
let rect = centered_fraction(area, 9, 10, 28, 5);
assert_eq!(rect, area);
}
/// `area.width * numerator` overflowed `u16` for a very wide area, which
/// panics in debug and silently wraps in release. The fraction must be
/// computed in a wider type instead.
#[test]
fn centered_fraction_survives_an_area_too_wide_for_u16_math() {
let area = Rect::new(0, 0, u16::MAX, u16::MAX);
let rect = centered_fraction(area, 9, 10, 28, 5);
let expected = u16::try_from(u32::from(u16::MAX) * 9 / 10)
.expect("nine tenths of u16::MAX still fits u16");
assert_eq!(rect.width, expected);
assert_eq!(rect.height, expected);
}
/// A zero denominator is a caller mistake, but it must not divide by zero.
#[test]
fn centered_fraction_treats_a_zero_denominator_as_the_full_area() {
let area = Rect::new(0, 0, 80, 24);
let rect = centered_fraction(area, 9, 0, 10, 4);
assert_eq!(rect, area);
}
}