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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//! Progress that reports what is actually known.
use gpui::{
App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
prelude::FluentBuilder, px, relative,
};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, Space};
use crate::foundation::Ident;
use crate::motion::{self, AnimationExt as _, MotionSpec};
use crate::strings::{ActiveStrings, StringKey};
/// What a progress surface knows about the work, and how it says so.
///
/// `ProgressBar` and [`crate::display::progress_circle::ProgressCircle`] draw
/// different shapes over exactly this state, so the two cannot drift into
/// telling different stories about the same work.
#[derive(Debug, Clone, Default, PartialEq)]
pub(crate) struct ProgressValue {
/// `None` means the extent of the work is unknown.
pub fraction: Option<f32>,
/// What to show beside the label, such as `"3 of 12"`, when the caller
/// worded it itself.
pub display: Option<SharedString>,
/// The work counted, when the caller gave a count rather than a wording.
/// It stays a pair of numbers until render, because the sentence around
/// them belongs to the installed catalogue and not to this struct.
pub count: Option<(usize, usize)>,
}
impl ProgressValue {
pub(crate) fn set_fraction(&mut self, fraction: f32) {
self.fraction = Some(fraction.clamp(0.0, 1.0));
}
/// Reports `done` out of `total`, and stays indeterminate when the total
/// is zero, because no fraction exists to report.
pub(crate) fn set_count(&mut self, done: usize, total: usize) {
self.fraction = (total > 0).then(|| (done as f32 / total as f32).clamp(0.0, 1.0));
self.count = Some((done, total));
}
pub(crate) fn is_indeterminate(&self) -> bool {
self.fraction.is_none()
}
/// The node both surfaces publish: busy always, a position only when the
/// extent is known.
/// What a reader sees beside the label: the caller's own wording first,
/// then a counted position worded by the catalogue.
pub(crate) fn shown(&self, cx: &App) -> Option<SharedString> {
self.display.clone().or_else(|| {
self.count.map(|(done, total)| {
cx.strings().format(
StringKey::CountOfTotal,
&[&done.to_string(), &total.to_string()],
)
})
})
}
pub(crate) fn spec(&self, id: SharedString, label: Option<SharedString>, cx: &App) -> NodeSpec {
let mut spec = NodeSpec::new(id, Role::Progress).busy(true);
if let Some(fraction) = self.fraction {
spec = spec.range(0.0, 1.0, fraction);
}
if let Some(label) = label {
spec = spec.text(label);
}
if let Some(display) = self.shown(cx) {
spec = spec.value(display);
}
spec
}
}
/// A horizontal bar for work whose extent is known.
///
/// A bar without a value is indeterminate and says so, rather than crawling
/// to ninety percent and waiting there.
#[derive(Debug, IntoElement)]
pub struct ProgressBar {
ident: Ident,
label: Option<SharedString>,
value: ProgressValue,
}
impl ProgressBar {
pub fn new(ident: impl Into<Ident>) -> Self {
Self {
ident: ident.into(),
label: None,
value: ProgressValue::default(),
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
/// How much of the work is done, between zero and one.
pub fn fraction(mut self, fraction: f32) -> Self {
self.value.set_fraction(fraction);
self
}
/// Reports `done` out of `total`, and stays indeterminate when the total
/// is zero, because no fraction exists to report.
pub fn count(mut self, done: usize, total: usize) -> Self {
self.value.set_count(done, total);
self
}
pub fn display(mut self, display: impl Into<SharedString>) -> Self {
self.value.display = Some(display.into());
self
}
}
impl RenderOnce for ProgressBar {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let indeterminate = self.value.is_indeterminate();
// The published range stays the caller's number from the frame it
// changes; only the fill takes its time getting there.
let drawn = self.value.fraction.map(|fraction| {
motion::tracked(
&self.ident.semantic_id(),
fraction,
motion::resize(&theme),
window,
cx,
)
});
let spec = self
.value
.spec(self.ident.semantic_id(), self.label.clone(), cx);
let display = self.value.shown(cx);
div()
.flex()
.flex_col()
.gap(px(theme.space(Space::Xs)))
.w_full()
.when_some(self.label.clone(), |element, label| {
element.child(
div()
.flex()
.flex_row()
.justify_between()
.text_size(px(theme.typography.body.size))
.text_color(theme.colors.text_muted)
.child(label)
.when_some(display, |element, display| {
element.child(div().text_color(theme.colors.text).child(display))
}),
)
})
.child(
div()
.relative()
.w_full()
.h(px(4.0))
.rounded_full()
.overflow_hidden()
.bg(theme.colors.hairline_strong)
.when_some(drawn, |element, fraction| {
element.child(
div()
.absolute()
.left_0()
.top_0()
.bottom_0()
.rounded_full()
.bg(theme.colors.accent)
.w(relative(fraction)),
)
})
// An unknown extent sweeps a segment across the track and
// tints nothing behind it. A partly filled bar would be
// read as a position and there is none, and a filled one
// reads as the position at the end: the still frame of a
// held animation showed exactly that, a bar that looked
// finished beside a run that had not started.
.when(indeterminate, |element| {
let period = MotionSpec::new(
theme.motion.pulse_ms * 2,
motion::CubicBezier::new(0.4, 0.0, 0.6, 1.0),
);
element.child(
div()
.absolute()
.top_0()
.bottom_0()
.w(relative(0.3))
.rounded_full()
.bg(theme.colors.accent)
.with_animation(
self.ident.child("sweep").element_id(),
period.repeating(),
|element, delta| element.left(relative(delta * 1.3 - 0.3)),
),
)
}),
)
.semantic_in(cx, spec)
}
}