Skip to main content

sequoia_elements/
progress.rs

1use std::time::Instant;
2
3use sequoia_core::{Color, Length, Rect};
4use sequoia_layout::LayoutStyle;
5use sequoia_reactive::Signal;
6use sequoia_view::{AnyView, PaintCtx, Panel, Pulse, Unit, View};
7
8#[derive(Clone)]
9pub struct ProgressBar {
10    value: Signal<f32>,
11    height: f32,
12    track: Option<Color>,
13    fill: Option<Color>,
14}
15
16impl ProgressBar {
17    pub fn new(value: Signal<f32>) -> Self {
18        Self {
19            value,
20            height: 6.0,
21            track: None,
22            fill: None,
23        }
24    }
25
26    pub fn height(mut self, h: f32) -> Self {
27        if h.is_finite() && h > 0.0 {
28            self.height = h;
29        }
30        self
31    }
32
33    pub fn track(mut self, color: Color) -> Self {
34        self.track = Some(color);
35        self
36    }
37
38    pub fn fill(mut self, color: Color) -> Self {
39        self.fill = Some(color);
40        self
41    }
42
43    fn clamped(&self) -> f32 {
44        self.value.get_untracked().clamp(0.0, 1.0)
45    }
46}
47
48impl View for ProgressBar {
49    fn body(&self) -> impl View + 'static {
50        Unit
51    }
52
53    fn layout_style(&self) -> LayoutStyle {
54        LayoutStyle {
55            width: Length::Fill(1.0),
56            height: Length::Px(self.height),
57            ..LayoutStyle::default()
58        }
59    }
60
61    fn paint_self(&self, ctx: &mut PaintCtx) {
62        let _ = self.value.get_untracked();
63        let theme = ctx.theme();
64        let track = self.track.unwrap_or(theme.surface);
65        let fill = self.fill.unwrap_or(theme.accent);
66        let r = ctx.rect;
67        ctx.quad(r, track, r.size.height * 0.5);
68        let w = r.size.width * self.clamped();
69        if w > 0.5 {
70            ctx.quad(
71                Rect::from_xywh(r.left(), r.top(), w, r.size.height),
72                fill,
73                r.size.height * 0.5,
74            );
75        }
76    }
77}
78
79#[derive(Clone)]
80pub struct Spinner {
81    size: f32,
82    color: Option<Color>,
83    period_secs: f32,
84}
85
86impl Spinner {
87    pub fn new() -> Self {
88        Self {
89            size: 16.0,
90            color: None,
91            period_secs: 1.0,
92        }
93    }
94
95    pub fn size(mut self, px: f32) -> Self {
96        if px.is_finite() && px > 0.0 {
97            self.size = px;
98        }
99        self
100    }
101
102    pub fn color(mut self, color: Color) -> Self {
103        self.color = Some(color);
104        self
105    }
106
107    pub fn period_secs(mut self, secs: f32) -> Self {
108        if secs.is_finite() && secs > 0.0 {
109            self.period_secs = secs;
110        }
111        self
112    }
113}
114
115impl Default for Spinner {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121impl View for Spinner {
122    fn body(&self) -> impl View + 'static {
123        Unit
124    }
125
126    fn layout_style(&self) -> LayoutStyle {
127        LayoutStyle {
128            width: Length::Px(self.size),
129            height: Length::Px(self.size),
130            ..LayoutStyle::default()
131        }
132    }
133
134    fn paint_self(&self, ctx: &mut PaintCtx) {
135        let theme = ctx.theme();
136        let base = self.color.unwrap_or(theme.accent);
137        let r = ctx.rect;
138        let cx = r.left() + r.size.width * 0.5;
139        let cy = r.top() + r.size.height * 0.5;
140        let radius = r.size.width.min(r.size.height) * 0.38;
141        let dot = (r.size.width.min(r.size.height) * 0.09).max(1.0);
142        let elapsed = START.elapsed().as_secs_f32();
143        let phase = (elapsed / self.period_secs) % 1.0;
144        for i in 0..8 {
145            let a = (i as f32 / 8.0) * std::f32::consts::TAU;
146
147            let dist = ((i as f32 / 8.0) - phase).rem_euclid(1.0);
148            let alpha = 1.0 - dist * 0.85;
149            ctx.quad(
150                Rect::from_xywh(
151                    cx + radius * a.cos() - dot,
152                    cy + radius * a.sin() - dot,
153                    dot * 2.0,
154                    dot * 2.0,
155                ),
156                base.with_alpha(alpha),
157                dot,
158            );
159        }
160        ctx.request_frame();
161    }
162}
163
164static START: std::sync::LazyLock<Instant> = std::sync::LazyLock::new(Instant::now);
165
166#[derive(Clone)]
167pub struct Skeleton {
168    width: Length,
169    height: f32,
170}
171
172impl Default for Skeleton {
173    fn default() -> Self {
174        Self::new()
175    }
176}
177
178impl Skeleton {
179    pub fn new() -> Self {
180        Self {
181            width: Length::Fill(1.0),
182            height: 16.0,
183        }
184    }
185
186    pub fn width(mut self, width: impl Into<Length>) -> Self {
187        self.width = width.into();
188        self
189    }
190
191    pub fn height(mut self, h: f32) -> Self {
192        if h.is_finite() && h > 0.0 {
193            self.height = h;
194        }
195        self
196    }
197}
198
199impl View for Skeleton {
200    fn body(&self) -> impl View + 'static {
201        Unit
202    }
203
204    fn children_override(&self) -> Option<Vec<AnyView>> {
205        Some(vec![AnyView::new(Pulse::new(
206            Panel::new()
207                .width(self.width)
208                .height(Length::Px(self.height)),
209        ))])
210    }
211
212    fn layout_style(&self) -> LayoutStyle {
213        LayoutStyle {
214            width: self.width,
215            height: Length::Px(self.height),
216            ..LayoutStyle::default()
217        }
218    }
219}