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
//! Where a scrollbar's thumb sits and how long it is.
//!
//! Pure arithmetic over three lengths — how much content there is, how much of
//! it is on screen, how far it has travelled — shared by every indicator that
//! reports a scroll position: the rectangular [`Scrollbar`](crate::widgets::Scrollbar)
//! and the curved indicator a round watch draws.
//!
//! What differs between them is only how short and how long the thumb is
//! allowed to get, which is why [`ThumbBounds`] is a parameter rather than a
//! constant here.
/// Thumb length and position, both as fractions of the track.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ThumbGeometry {
/// The thumb's share of the track, in `0..=1`.
pub length: f32,
/// The thumb's leading edge: `0.0` at the start of the track, and
/// `1.0 - length` once the content is scrolled to the end.
pub offset: f32,
}
impl ThumbGeometry {
/// The thumb's trailing edge, as a fraction of the track.
pub fn end(self) -> f32 {
self.offset + self.length
}
/// How far through its travel the thumb is, in `0..=1`. `0.0` when the
/// thumb fills the track and cannot travel at all.
pub fn progress(self) -> f32 {
let travel = 1.0 - self.length;
if travel <= 0.0 {
0.0
} else {
(self.offset / travel).clamp(0.0, 1.0)
}
}
}
/// How short and how long a thumb may get, as fractions of the track.
///
/// A very long list would otherwise produce a thumb too small to see or to
/// grab; a platform states its own floor rather than inheriting someone else's.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ThumbBounds {
minimum: f32,
maximum: f32,
}
impl ThumbBounds {
/// A thumb free to be any length, which is what a caller that enforces a
/// minimum in pixels rather than fractions wants.
pub const FULL: Self = Self {
minimum: 0.0,
maximum: 1.0,
};
/// Bounds clamped into `0..=1` and ordered, so a caller cannot describe an
/// empty range.
pub fn new(minimum: f32, maximum: f32) -> Self {
let minimum = if minimum.is_finite() {
minimum.clamp(0.0, 1.0)
} else {
0.0
};
let maximum = if maximum.is_finite() {
maximum.clamp(0.0, 1.0)
} else {
1.0
};
Self {
minimum: minimum.min(maximum),
maximum: maximum.max(minimum),
}
}
/// The bounds that keep a thumb at least `extent` long on a track of
/// `track` — the pixel-shaped way to say it.
pub fn at_least(extent: f32, track: f32) -> Self {
if !(extent.is_finite() && track.is_finite()) || track <= 0.0 || extent <= 0.0 {
return Self::FULL;
}
Self::new(extent / track, 1.0)
}
/// Shortest permitted thumb, as a fraction of the track.
pub fn minimum(self) -> f32 {
self.minimum
}
/// Longest permitted thumb, as a fraction of the track.
pub fn maximum(self) -> f32 {
self.maximum
}
}
impl Default for ThumbBounds {
fn default() -> Self {
Self::FULL
}
}
/// The thumb for a scroll position, or `None` when the content fits and there
/// is nothing to indicate.
///
/// `content` and `viewport` are lengths in one unit and `scrolled` is how far
/// the content has travelled in that same unit. Non-finite inputs and a
/// viewport that has not been measured yet both answer `None` rather than a
/// thumb drawn from a guess.
pub fn thumb_geometry(
content: f32,
viewport: f32,
scrolled: f32,
bounds: ThumbBounds,
) -> Option<ThumbGeometry> {
if !(content.is_finite() && viewport.is_finite() && scrolled.is_finite()) {
return None;
}
if viewport <= 0.0 || content <= viewport {
return None;
}
let length = (viewport / content).clamp(bounds.minimum(), bounds.maximum());
let travel = content - viewport;
let progress = (scrolled / travel).clamp(0.0, 1.0);
Some(ThumbGeometry {
length,
offset: progress * (1.0 - length),
})
}
/// How far the content moves when the thumb is dragged `delta` along a track of
/// `track`, given the thumb it currently shows.
///
/// A thumb that fills its track cannot be dragged, and a scroll with no travel
/// cannot follow one, so both answer zero rather than dividing by zero.
pub fn content_delta_for_thumb_drag(
delta: f32,
track: f32,
geometry: ThumbGeometry,
max_offset: f32,
) -> f32 {
if !(delta.is_finite() && track.is_finite() && max_offset.is_finite()) {
return 0.0;
}
let travel = track * (1.0 - geometry.length);
if travel <= 0.0 || max_offset <= 0.0 {
return 0.0;
}
delta / travel * max_offset
}
#[cfg(test)]
#[path = "tests/scrollbar_tests.rs"]
mod tests;