cadmus_core/view/
progress_bar.rs

1use super::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER};
2use crate::color::{BLACK, PROGRESS_EMPTY, PROGRESS_FULL, WHITE};
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::font::Fonts;
6use crate::framebuffer::{Framebuffer, UpdateMode};
7use crate::geom::{halves, BorderSpec, CornerSpec, Rectangle};
8use crate::unit::scale_by_dpi;
9
10const PROGRESS_HEIGHT: f32 = 7.0;
11const BORDER_THICKNESS: f32 = 1.0;
12
13/// A read-only horizontal progress bar.
14///
15/// Displays a filled track proportional to the current `percent` value (0–100).
16/// Unlike [`Slider`](super::slider::Slider), this view is non-interactive and
17/// has no draggable thumb — it is intended for displaying download or
18/// installation progress.
19pub struct ProgressBar {
20    id: Id,
21    rect: Rectangle,
22    children: Vec<Box<dyn View>>,
23    percent: u8,
24}
25
26impl ProgressBar {
27    /// Creates a new progress bar at the given rect with an initial percent (0–100).
28    pub fn new(rect: Rectangle, percent: u8) -> ProgressBar {
29        ProgressBar {
30            id: ID_FEEDER.next(),
31            rect,
32            children: Vec::new(),
33            percent: percent.min(100),
34        }
35    }
36
37    /// Updates the progress value and queues a re-render if the value changed.
38    pub fn update(&mut self, percent: u8, rq: &mut RenderQueue) {
39        let clamped = percent.min(100);
40
41        if self.percent != clamped {
42            self.percent = clamped;
43            rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
44        }
45    }
46}
47
48impl View for ProgressBar {
49    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level = tracing::Level::TRACE)))]
50    fn handle_event(
51        &mut self,
52        _evt: &Event,
53        _hub: &Hub,
54        _bus: &mut Bus,
55        _rq: &mut RenderQueue,
56        _context: &mut Context,
57    ) -> bool {
58        false
59    }
60
61    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, _fonts, _rect), fields(rect = ?_rect)))]
62    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {
63        let dpi = CURRENT_DEVICE.dpi;
64        let progress_height = scale_by_dpi(PROGRESS_HEIGHT, dpi) as i32;
65        let border_thickness = scale_by_dpi(BORDER_THICKNESS, dpi) as u16;
66
67        fb.draw_rectangle(&self.rect, WHITE);
68
69        let (small_half, _big_half) = halves(progress_height);
70        let (small_padding, big_padding) = halves(self.rect.height() as i32 - progress_height);
71
72        let track_rect = rect![
73            self.rect.min.x,
74            self.rect.min.y + small_padding,
75            self.rect.max.x,
76            self.rect.max.y - big_padding
77        ];
78
79        let fill_x =
80            self.rect.min.x + (self.rect.width() as f32 * self.percent as f32 / 100.0) as i32;
81
82        fb.draw_rounded_rectangle_with_border(
83            &track_rect,
84            &CornerSpec::Uniform(small_half),
85            &BorderSpec {
86                thickness: border_thickness,
87                color: BLACK,
88            },
89            &|x, _| {
90                if x < fill_x {
91                    PROGRESS_FULL
92                } else {
93                    PROGRESS_EMPTY
94                }
95            },
96        );
97    }
98
99    fn rect(&self) -> &Rectangle {
100        &self.rect
101    }
102
103    fn rect_mut(&mut self) -> &mut Rectangle {
104        &mut self.rect
105    }
106
107    fn children(&self) -> &Vec<Box<dyn View>> {
108        &self.children
109    }
110
111    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
112        &mut self.children
113    }
114
115    fn id(&self) -> Id {
116        self.id
117    }
118}