gpui_kit/display/
progress.rs1use gpui::{
4 App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
5 prelude::FluentBuilder, px, relative,
6};
7use gpui_kit_semantics::{NodeSpec, Role, Semantic};
8use gpui_kit_theme::{ActiveTheme, Space};
9
10use crate::foundation::Ident;
11use crate::motion::{self, AnimationExt as _, MotionSpec};
12use crate::strings::{ActiveStrings, StringKey};
13
14#[derive(Debug, Clone, Default, PartialEq)]
20pub(crate) struct ProgressValue {
21 pub fraction: Option<f32>,
23 pub display: Option<SharedString>,
26 pub count: Option<(usize, usize)>,
30}
31
32impl ProgressValue {
33 pub(crate) fn set_fraction(&mut self, fraction: f32) {
34 self.fraction = Some(fraction.clamp(0.0, 1.0));
35 }
36
37 pub(crate) fn set_count(&mut self, done: usize, total: usize) {
40 self.fraction = (total > 0).then(|| (done as f32 / total as f32).clamp(0.0, 1.0));
41 self.count = Some((done, total));
42 }
43
44 pub(crate) fn is_indeterminate(&self) -> bool {
45 self.fraction.is_none()
46 }
47
48 pub(crate) fn shown(&self, cx: &App) -> Option<SharedString> {
53 self.display.clone().or_else(|| {
54 self.count.map(|(done, total)| {
55 cx.strings().format(
56 StringKey::CountOfTotal,
57 &[&done.to_string(), &total.to_string()],
58 )
59 })
60 })
61 }
62
63 pub(crate) fn spec(&self, id: SharedString, label: Option<SharedString>, cx: &App) -> NodeSpec {
64 let mut spec = NodeSpec::new(id, Role::Progress).busy(true);
65 if let Some(fraction) = self.fraction {
66 spec = spec.range(0.0, 1.0, fraction);
67 }
68 if let Some(label) = label {
69 spec = spec.text(label);
70 }
71 if let Some(display) = self.shown(cx) {
72 spec = spec.value(display);
73 }
74 spec
75 }
76}
77
78#[derive(Debug, IntoElement)]
83pub struct ProgressBar {
84 ident: Ident,
85 label: Option<SharedString>,
86 value: ProgressValue,
87}
88
89impl ProgressBar {
90 pub fn new(ident: impl Into<Ident>) -> Self {
91 Self {
92 ident: ident.into(),
93 label: None,
94 value: ProgressValue::default(),
95 }
96 }
97
98 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
99 self.label = Some(label.into());
100 self
101 }
102
103 pub fn fraction(mut self, fraction: f32) -> Self {
105 self.value.set_fraction(fraction);
106 self
107 }
108
109 pub fn count(mut self, done: usize, total: usize) -> Self {
112 self.value.set_count(done, total);
113 self
114 }
115
116 pub fn display(mut self, display: impl Into<SharedString>) -> Self {
117 self.value.display = Some(display.into());
118 self
119 }
120}
121
122impl RenderOnce for ProgressBar {
123 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
124 let theme = cx.theme().clone();
125 let indeterminate = self.value.is_indeterminate();
126 let drawn = self.value.fraction.map(|fraction| {
129 motion::tracked(
130 &self.ident.semantic_id(),
131 fraction,
132 motion::resize(&theme),
133 window,
134 cx,
135 )
136 });
137
138 let spec = self
139 .value
140 .spec(self.ident.semantic_id(), self.label.clone(), cx);
141 let display = self.value.shown(cx);
142
143 div()
144 .flex()
145 .flex_col()
146 .gap(px(theme.space(Space::Xs)))
147 .w_full()
148 .when_some(self.label.clone(), |element, label| {
149 element.child(
150 div()
151 .flex()
152 .flex_row()
153 .justify_between()
154 .text_size(px(theme.typography.body.size))
155 .text_color(theme.colors.text_muted)
156 .child(label)
157 .when_some(display, |element, display| {
158 element.child(div().text_color(theme.colors.text).child(display))
159 }),
160 )
161 })
162 .child(
163 div()
164 .relative()
165 .w_full()
166 .h(px(4.0))
167 .rounded_full()
168 .overflow_hidden()
169 .bg(theme.colors.hairline_strong)
170 .when_some(drawn, |element, fraction| {
171 element.child(
172 div()
173 .absolute()
174 .left_0()
175 .top_0()
176 .bottom_0()
177 .rounded_full()
178 .bg(theme.colors.accent)
179 .w(relative(fraction)),
180 )
181 })
182 .when(indeterminate, |element| {
189 let period = MotionSpec::new(
190 theme.motion.pulse_ms * 2,
191 motion::CubicBezier::new(0.4, 0.0, 0.6, 1.0),
192 );
193 element.child(
194 div()
195 .absolute()
196 .top_0()
197 .bottom_0()
198 .w(relative(0.3))
199 .rounded_full()
200 .bg(theme.colors.accent)
201 .with_animation(
202 self.ident.child("sweep").element_id(),
203 period.repeating(),
204 |element, delta| element.left(relative(delta * 1.3 - 0.3)),
205 ),
206 )
207 }),
208 )
209 .semantic_in(cx, spec)
210 }
211}