Skip to main content

recovery_ui/
progress_bar.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::Error;
6use carnelian::color::Color;
7use carnelian::drawing::{FontFace, load_font, measure_text_width};
8use carnelian::scene::facets::{
9    FacetId, SetColorMessage, SetSizeMessage, SetTextMessage, TextFacetOptions,
10    TextHorizontalAlignment, TextVerticalAlignment,
11};
12use carnelian::scene::layout::{Alignment, Stack, StackOptions};
13use carnelian::scene::scene::{Scene, SceneBuilder};
14use carnelian::{AppSender, MessageTarget, Point, Size, ViewKey, make_message};
15use euclid::size2;
16use fuchsia_async as fasync;
17use fuchsia_sync::Mutex;
18use futures::StreamExt;
19use futures::channel::mpsc::{Sender, channel as pipe};
20use std::path::PathBuf;
21use std::sync::Arc;
22use std::sync::atomic::{AtomicU32, Ordering};
23use zx::MonotonicDuration;
24
25const PROGRESS_GRANULARITY: f32 = 1000.0;
26
27// We calculate the size from a provided size or displayed text, we must have at least one
28pub enum ProgressBarConfig {
29    Text(ProgressBarText),
30    Size(Size),
31    TextWithSize(ProgressBarText, Size),
32}
33
34// Progress is [0.0..1.0[
35pub enum ProgressBarMessages {
36    SetProgress(/* progress*/ f32),
37    SetProgressSmooth(
38        /* progress */ f32,
39        /* time to get to progress */ MonotonicDuration,
40    ),
41    SetInternalProgress(f32),
42    SetProgressBarText(String),
43}
44
45pub struct ProgressBar {
46    label: Option<FacetId>,
47    progress_bar_size: Size,
48    progress_rectangle: FacetId,
49    background_rectangle: FacetId,
50    app_sender: AppSender,
51    // Progress stored as 0 to PROGRESS_GRANULARITY
52    current_progress: Arc<AtomicU32>,
53    // Progress stored as 0 to PROGRESS_GRANULARITY
54    final_progress: Arc<AtomicU32>,
55    // To make sure that we are only starting one copy of the background task
56    task_running_mutex: Arc<Mutex<bool>>,
57    // Channel to send desired progress to the background task
58    task_sender: Option<Sender<(/* progress */ u32, /* step_time_ms */ i64)>>,
59}
60
61#[derive(Clone)]
62pub struct ProgressBarText {
63    pub text: String,
64    pub font_size: f32,
65    pub padding: f32,
66    pub alignment: TextHorizontalAlignment,
67}
68
69impl ProgressBar {
70    pub fn new(
71        config: ProgressBarConfig,
72        builder: &mut SceneBuilder,
73        app_sender: AppSender,
74    ) -> Result<ProgressBar, Error> {
75        let options =
76            StackOptions { alignment: Alignment::center_left(), ..StackOptions::default() };
77
78        // Construct the progress bar itself
79        builder.start_group("progress_bar", Stack::with_options_ptr(options));
80
81        let (label, progress_bar_size) = build_progress_text(config, builder)?;
82        let progress_rectangle = builder.rectangle(
83            size2(0.0, progress_bar_size.height),
84            Color { r: 0x0b, g: 0x57, b: 0xd0, a: 0xff },
85        );
86        let background_rectangle =
87            builder.rectangle(progress_bar_size, Color { r: 0xbe, g: 0xd5, b: 0xfc, a: 0xff });
88        builder.end_group();
89
90        Ok(ProgressBar {
91            label,
92            progress_bar_size,
93            progress_rectangle,
94            background_rectangle,
95            app_sender,
96            current_progress: Arc::new(AtomicU32::new(0)),
97            final_progress: Arc::new(AtomicU32::new(0)),
98            task_running_mutex: Arc::new(Mutex::new(false)),
99            task_sender: None,
100        })
101    }
102
103    /// Set progress between 0 to 100 percent
104    pub fn set_percent(&mut self, scene: &mut Scene, percent_complete: f32) {
105        self.set_progress(scene, percent_complete / 100.0);
106    }
107
108    /// Set progress from 0.0 to 1.0
109    pub fn set_progress(&mut self, scene: &mut Scene, progress: f32) {
110        self.final_progress.store((progress * PROGRESS_GRANULARITY) as u32, Ordering::Release);
111        self.current_progress.store((progress * PROGRESS_GRANULARITY) as u32, Ordering::Release);
112        self.set_internal_progress(scene, progress);
113    }
114
115    /// Set progress from 0 to 1.0
116    pub fn set_internal_progress(&mut self, scene: &mut Scene, progress: f32) {
117        let progress = num_traits::clamp(progress, 0.0, 1.0);
118        let width = self.progress_bar_size.width;
119        let height = self.progress_bar_size.height;
120        let filled = width * progress;
121        scene.send_message(
122            &self.progress_rectangle,
123            Box::new(SetSizeMessage { size: size2(filled, height) }),
124        );
125    }
126
127    pub fn set_percent_smooth(&mut self, view_key: ViewKey, percent_complete: f32) {
128        self.set_progress_smooth(
129            view_key,
130            percent_complete / 100.0,
131            MonotonicDuration::from_seconds(1),
132        );
133    }
134
135    /// Set progress from 0.0 to 1.0
136    pub fn set_progress_smooth(
137        &mut self,
138        view_key: ViewKey,
139        progress: f32,
140        elapsed_time: MonotonicDuration,
141    ) {
142        let progress = (progress * PROGRESS_GRANULARITY) as u32;
143        let current_progress = self.current_progress.load(Ordering::Acquire);
144        let step_time_ms = (elapsed_time.into_millis()
145            / (current_progress as i32 - progress as i32).abs() as i64)
146            as i64;
147        let mut running = self.task_running_mutex.lock();
148        if !*running {
149            *running = true;
150            let app_sender = self.app_sender.clone();
151            let current_progress = self.current_progress.clone();
152            let final_progress = self.final_progress.clone();
153            let (tx, mut rx) = pipe::<(u32, i64)>(1);
154            self.task_sender = Some(tx);
155            let f = async move {
156                let mut sleep_time = MonotonicDuration::from_millis(step_time_ms);
157                loop {
158                    let current = current_progress.load(Ordering::Acquire);
159
160                    let end = final_progress.load(Ordering::Acquire);
161                    let difference = end as i64 - current as i64;
162                    if difference == 0 {
163                        let (progress, step_time_ms) = rx.next().await.unwrap();
164                        sleep_time = MonotonicDuration::from_millis(step_time_ms);
165                        final_progress.store(progress as u32, Ordering::Release);
166                    } else {
167                        match rx.try_next() {
168                            Ok(value) => {
169                                if let Some((progress, step_time_ms)) = value {
170                                    sleep_time = MonotonicDuration::from_millis(step_time_ms);
171                                    final_progress.store(progress as u32, Ordering::Release);
172                                }
173                            }
174                            Err(_) => { // Ignore
175                            }
176                        }
177                        if difference > 0 {
178                            current_progress.fetch_add(1, Ordering::AcqRel);
179                        } else {
180                            current_progress.fetch_sub(1, Ordering::AcqRel);
181                        }
182                        app_sender.queue_message(
183                            MessageTarget::View(view_key),
184                            make_message(ProgressBarMessages::SetInternalProgress(
185                                current_progress.load(Ordering::Acquire) as f32
186                                    / PROGRESS_GRANULARITY,
187                            )),
188                        );
189                        fuchsia_async::Timer::new(sleep_time).await;
190                    }
191                }
192            };
193            fasync::Task::local(f).detach();
194        }
195        let _ = self.task_sender.as_mut().unwrap().start_send((progress, step_time_ms));
196    }
197
198    pub fn set_color(&mut self, scene: &mut Scene, fg_color: Color, bg_color: Color) {
199        scene.send_message(&self.progress_rectangle, Box::new(SetColorMessage { color: fg_color }));
200        scene.send_message(
201            &self.background_rectangle,
202            Box::new(SetColorMessage { color: bg_color }),
203        );
204    }
205
206    pub fn set_text(&mut self, scene: &mut Scene, text: String) {
207        if let Some(label) = self.label.as_ref() {
208            scene.send_message(label, Box::new(SetTextMessage { text }));
209        }
210    }
211
212    pub fn set_text_color(&mut self, scene: &mut Scene, color: Color) {
213        if let Some(label) = self.label.as_ref() {
214            scene.send_message(label, Box::new(SetColorMessage { color }));
215        }
216    }
217
218    pub fn handle_message(
219        &mut self,
220        scene: &mut Scene,
221        view_key: ViewKey,
222        message: ProgressBarMessages,
223    ) {
224        match message {
225            ProgressBarMessages::SetProgress(progress) => {
226                self.set_progress(scene, progress);
227            }
228            ProgressBarMessages::SetProgressSmooth(progress, step_time_ms) => {
229                self.set_progress_smooth(view_key, progress, step_time_ms);
230            }
231            ProgressBarMessages::SetInternalProgress(progress) => {
232                self.set_internal_progress(scene, progress);
233            }
234            ProgressBarMessages::SetProgressBarText(text) => {
235                self.set_text(scene, text);
236            }
237        }
238    }
239}
240
241fn build_text_facet(
242    builder: &mut SceneBuilder,
243    progress_bar_text: &ProgressBarText,
244    face: FontFace,
245) -> Result<FacetId, Error> {
246    let label = builder.text(
247        face,
248        &progress_bar_text.text,
249        progress_bar_text.font_size,
250        Point::zero(),
251        TextFacetOptions {
252            color: Color::blue(),
253            horizontal_alignment: progress_bar_text.alignment,
254            vertical_alignment: TextVerticalAlignment::Top,
255            ..TextFacetOptions::default()
256        },
257    );
258    Ok(label)
259}
260
261fn build_progress_text(
262    config: ProgressBarConfig,
263    builder: &mut SceneBuilder,
264) -> Result<(Option<FacetId>, Size), Error> {
265    Ok(match config {
266        ProgressBarConfig::Text(progress_bar_text) => {
267            let face = load_font(PathBuf::from("/pkg/data/fonts/Roboto-Regular.ttf"))?;
268            let label_width =
269                measure_text_width(&face, progress_bar_text.font_size, &progress_bar_text.text);
270            let width = label_width + progress_bar_text.padding * 2.0;
271            let height = progress_bar_text.font_size + progress_bar_text.padding * 2.0;
272            let size = size2(width, height);
273            let label = build_text_facet(builder, &progress_bar_text, face)?;
274            (Some(label), size)
275        }
276        ProgressBarConfig::TextWithSize(progress_bar_text, size) => {
277            let face = load_font(PathBuf::from("/pkg/data/fonts/Roboto-Regular.ttf"))?;
278            let label = build_text_facet(builder, &progress_bar_text, face)?;
279            (Some(label), size)
280        }
281        ProgressBarConfig::Size(size) => (None, size),
282    })
283}