gpui_kit_grid/state/progress.rs
1//! Progress: the progress shown over the body and its overlay.
2//!
3//! Two sources: the view ([`LiveGridState::set_progress`], UI thread) and
4//! the store (`GridWriter::set_progress`, any thread, in order with the
5//! data). The view's progress wins; an application that wants another rule
6//! combines both itself. The store's progress is ordered with the data:
7//! when the store clears it at version V while the order on screen is still
8//! built from an older version, the view keeps showing it until the order
9//! covers V (the coverage `flush` waits for). So the bar does not leave
10//! before the rows it announced are shown.
11//!
12//! The overlay is absolute over the body, so it moves no row when it comes
13//! or goes, and it has no hitbox: presses and the wheel reach the body.
14
15use gpui_kit::base::TestSupportExt as _;
16use gpui_kit::component::{ActiveTheme as _, Sizable as _, progress::Progress};
17use gpui_kit::{
18 AnyElement, App, Context, InteractiveElement as _, IntoElement, ParentElement as _,
19 SharedString, Styled as _, Window, div, prelude::FluentBuilder as _,
20};
21use rust_i18n::t;
22
23use super::{LiveGridDelegate, LiveGridState};
24use crate::store::GridProgress;
25
26/// The element id of the overlay's container.
27const OVERLAY_ID: &str = "live-grid-progress";
28/// The element id of the default overlay's label on an empty grid.
29const LABEL_ID: &str = "live-grid-progress-label";
30
31/// What [`LiveGridDelegate::render_progress`] knows about the grid.
32///
33/// [`LiveGridDelegate::render_progress`]: crate::LiveGridDelegate::render_progress
34pub struct ProgressContext {
35 empty: bool,
36}
37
38impl ProgressContext {
39 /// Returns true if the grid shows no data rows, as during a first load.
40 pub fn is_empty(&self) -> bool {
41 self.empty
42 }
43
44 /// The default presentation: a 4 px bar (gpui-kit's `Progress`,
45 /// `xsmall`) across the grid right under the header, over the pinned
46 /// rows and the data rows; indeterminate when the length of the work is
47 /// unknown. On an empty grid the progress label, or "Loading…", is
48 /// centered in the body as muted text. With data shown there is no
49 /// label: the previous rows stay readable while they refresh (Design
50 /// Guides, "Loading: preserve context").
51 ///
52 /// The label is also the bar's accessible name. gpui-kit's `Progress`
53 /// honors reduced motion: an indeterminate bar does not animate then.
54 pub fn default_overlay(
55 &self,
56 progress: &GridProgress,
57 _window: &mut Window,
58 cx: &mut App,
59 ) -> AnyElement {
60 let label = progress_label(progress);
61 div()
62 .relative()
63 .size_full()
64 .child(
65 Progress::new("live-grid-progress-bar")
66 .xsmall()
67 .loading(progress.is_indeterminate())
68 .value(progress.fraction().unwrap_or_default() * 100.)
69 .accessibility_label(label.clone())
70 // Edge to edge under the header's line.
71 .rounded_none()
72 .absolute()
73 .top_0()
74 .left_0()
75 .right_0(),
76 )
77 .when(self.empty, |this| {
78 this.child(
79 div()
80 .id(LABEL_ID)
81 .test_support()
82 .absolute()
83 .size_full()
84 .flex()
85 .items_center()
86 .justify_center()
87 .text_color(cx.theme().muted_foreground)
88 .child(label),
89 )
90 })
91 .into_any_element()
92 }
93}
94
95/// The text of a progress: its label, or "Loading…".
96fn progress_label(progress: &GridProgress) -> SharedString {
97 match progress.label() {
98 Some(label) => SharedString::from(label.to_owned()),
99 None => SharedString::from(t!("live_grid.progress.loading").into_owned()),
100 }
101}
102
103/// The progress of the view and the store's progress as shown.
104#[derive(Default)]
105pub(super) struct ProgressSlot {
106 /// [`LiveGridState::set_progress`].
107 view: Option<GridProgress>,
108 /// The store's progress as shown: kept after the store cleared it,
109 /// until the order on screen covers the clearing.
110 store: Option<GridProgress>,
111 /// The store version that cleared `store`, while it is still shown.
112 cleared_at: Option<u64>,
113}
114
115impl ProgressSlot {
116 /// A slot that shows the store's progress `store` from the start.
117 pub(super) fn new(store: Option<&GridProgress>) -> Self {
118 Self {
119 view: None,
120 store: store.cloned(),
121 cleared_at: None,
122 }
123 }
124
125 /// The progress shown: the view's, else the store's.
126 fn shown(&self) -> Option<&GridProgress> {
127 self.view.as_ref().or(self.store.as_ref())
128 }
129
130 /// A snapshot of `version` changed the store's progress to `progress`.
131 /// New progress shows at once; cleared progress stays until
132 /// [`Self::release`] sees the order cover `version`.
133 pub(super) fn follow_store(&mut self, progress: Option<&GridProgress>, version: u64) {
134 match progress {
135 Some(progress) => {
136 if self.store.as_ref() != Some(progress) {
137 self.store = Some(progress.clone());
138 }
139 self.cleared_at = None;
140 }
141 None if self.store.is_some() && self.cleared_at.is_none() => {
142 self.cleared_at = Some(version);
143 }
144 None => {}
145 }
146 }
147
148 /// The order on screen covers `coverage`. Returns true if a cleared
149 /// store progress left with it.
150 pub(super) fn release(&mut self, coverage: u64) -> bool {
151 match self.cleared_at {
152 Some(version) if coverage >= version => {
153 self.store = None;
154 self.cleared_at = None;
155 true
156 }
157 _ => false,
158 }
159 }
160}
161
162impl<D: LiveGridDelegate> LiveGridState<D> {
163 /// Shows the view's own progress over the store's; `None` shows the
164 /// store's again. For work the view waits for itself; a producer writes
165 /// `GridWriter::set_progress` instead, in order with its data.
166 pub fn set_progress(&mut self, progress: Option<GridProgress>, cx: &mut Context<Self>) {
167 if self.progress.view != progress {
168 self.progress.view = progress;
169 cx.notify();
170 }
171 }
172
173 /// The progress shown now: the view's ([`Self::set_progress`]), else the
174 /// store's. Progress the store cleared stays shown until the order on
175 /// screen reflects every write before the clearing, so the bar does not
176 /// leave before the new rows show.
177 pub fn progress(&self) -> Option<&GridProgress> {
178 self.progress.shown()
179 }
180
181 /// The overlay of the shown progress, from the delegate's
182 /// `render_progress`: called in `render`, only while progress is shown.
183 /// Absolute over the body, below the header.
184 pub(super) fn render_progress_overlay(
185 &self,
186 window: &mut Window,
187 cx: &mut App,
188 ) -> Option<impl IntoElement + use<D>> {
189 let progress = self.progress()?;
190 let context = ProgressContext {
191 empty: self.projection.row_count() == 0,
192 };
193 let overlay = self
194 .delegate
195 .render_progress(progress, &context, window, cx);
196 Some(
197 div()
198 .id(OVERLAY_ID)
199 .test_support()
200 .absolute()
201 // Below the group panel too.
202 .top(self.body_top())
203 .left_0()
204 .right_0()
205 .bottom_0()
206 .child(overlay),
207 )
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::{GridProgress, ProgressSlot, progress_label};
214
215 #[test]
216 fn test_progress_slot() {
217 let loading = GridProgress::indeterminate().with_label("Loading trades");
218 let mut slot = ProgressSlot::new(None);
219 assert_eq!(slot.shown(), None);
220
221 // The store's progress shows at once.
222 slot.follow_store(Some(&loading), 3);
223 assert_eq!(slot.shown(), Some(&loading));
224 // Cleared at version 5: shown until the order covers 5.
225 slot.follow_store(None, 5);
226 assert!(!slot.release(4));
227 assert_eq!(slot.shown(), Some(&loading));
228 assert!(slot.release(5));
229 assert_eq!(slot.shown(), None);
230 assert!(!slot.release(6));
231
232 // New progress replaces a cleared one waiting for the order.
233 let half = GridProgress::new(0.5);
234 slot.follow_store(Some(&loading), 7);
235 slot.follow_store(None, 8);
236 slot.follow_store(Some(&half), 9);
237 assert!(!slot.release(9));
238 assert_eq!(slot.shown(), Some(&half));
239
240 // The view's progress wins over the store's.
241 slot.view = Some(loading.clone());
242 assert_eq!(slot.shown(), Some(&loading));
243 slot.view = None;
244 assert_eq!(slot.shown(), Some(&half));
245
246 assert_eq!(ProgressSlot::new(Some(&half)).shown(), Some(&half));
247 assert_eq!(progress_label(&loading), "Loading trades");
248 assert_eq!(
249 &*progress_label(&half),
250 &*rust_i18n::t!("live_grid.progress.loading")
251 );
252 }
253}