damascene_core/widgets/
progress.rs1#![warn(missing_docs)]
34
35use std::panic::Location;
36
37use crate::layout::LayoutCtx;
38use crate::metrics::MetricsRole;
39use crate::shader::{ShaderBinding, StockShader, UniformValue};
40use crate::tokens;
41use crate::tree::*;
42
43pub const DEFAULT_HEIGHT: f32 = 8.0;
45
46#[track_caller]
56pub fn progress(value: f32) -> El {
57 progress_with_color(value, tokens::PRIMARY)
58}
59
60#[track_caller]
63pub fn progress_with_color(value: f32, fill_color: Color) -> El {
64 let value = value.clamp(0.0, 1.0);
65 let layout = move |ctx: LayoutCtx| {
66 let r = ctx.container;
67 vec![
68 Rect::new(r.x, r.y, r.w, r.h),
70 Rect::new(r.x, r.y, r.w * value, r.h),
72 ]
73 };
74
75 El::new(Kind::Custom("progress"))
76 .axis(Axis::Overlay)
77 .children([
78 El::new(Kind::Custom("progress-track"))
79 .fill(tokens::MUTED)
80 .radius(tokens::RADIUS_PILL),
81 El::new(Kind::Custom("progress-fill"))
82 .fill(fill_color)
83 .radius(tokens::RADIUS_PILL),
84 ])
85 .at_loc(Location::caller())
86 .metrics_role(MetricsRole::Progress)
87 .layout(layout)
88 .width(Size::Fill(1.0))
89 .default_height(Size::Fixed(DEFAULT_HEIGHT))
90}
91
92#[track_caller]
111pub fn progress_indeterminate() -> El {
112 progress_indeterminate_with_color(tokens::PRIMARY)
113}
114
115#[track_caller]
118pub fn progress_indeterminate_with_color(bar_color: Color) -> El {
119 let binding = ShaderBinding::stock(StockShader::ProgressIndeterminate)
120 .with("vec_a", UniformValue::Color(bar_color))
121 .with("vec_b", UniformValue::Color(tokens::MUTED))
122 .with(
127 "vec_c",
128 UniformValue::Vec4([tokens::RADIUS_PILL, 0.0, 0.0, 0.0]),
129 );
130
131 El::new(Kind::Custom("progress-indeterminate"))
132 .at_loc(Location::caller())
133 .shader(binding)
134 .metrics_role(MetricsRole::Progress)
135 .width(Size::Fill(1.0))
136 .default_height(Size::Fixed(DEFAULT_HEIGHT))
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142
143 #[test]
144 fn track_and_fill_use_expected_tokens() {
145 let p = progress(0.5);
146 assert_eq!(p.children.len(), 2);
147 assert_eq!(p.children[0].fill, Some(tokens::MUTED), "track is muted");
148 assert_eq!(
149 p.children[1].fill,
150 Some(tokens::PRIMARY),
151 "fill uses caller's color"
152 );
153 assert_eq!(
155 p.children[0].radius,
156 crate::tree::Corners::all(tokens::RADIUS_PILL)
157 );
158 assert_eq!(
159 p.children[1].radius,
160 crate::tree::Corners::all(tokens::RADIUS_PILL)
161 );
162 }
163
164 #[test]
165 fn layout_clamps_value_below_zero() {
166 use crate::layout::layout;
169 use crate::state::UiState;
170
171 let mut tree = progress(-0.5);
172 let mut state = UiState::new();
173 let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
174 layout(&mut tree, &mut state, viewport);
175 let fill_rect = tree.children[1].computed_rect;
176 assert_eq!(fill_rect.w, 0.0, "negative values clamp to empty fill");
177 }
178
179 #[test]
180 fn layout_clamps_value_above_one() {
181 use crate::layout::layout;
182 use crate::state::UiState;
183
184 let mut tree = progress(1.5);
185 let mut state = UiState::new();
186 let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
187 layout(&mut tree, &mut state, viewport);
188 let fill_rect = tree.children[1].computed_rect;
189 assert_eq!(fill_rect.w, 200.0, "values above 1.0 clamp to full track");
190 }
191
192 #[test]
193 fn indeterminate_binds_stock_shader() {
194 use crate::shader::ShaderHandle;
195 let p = progress_indeterminate();
196 let binding = p.shader_override.as_ref().expect("shader binding");
197 assert_eq!(
198 binding.handle,
199 ShaderHandle::Stock(StockShader::ProgressIndeterminate),
200 "progress_indeterminate must paint through stock::progress_indeterminate",
201 );
202 match binding.uniforms.get("vec_a") {
203 Some(UniformValue::Color(c)) => assert_eq!(*c, tokens::PRIMARY),
204 other => panic!("expected vec_a=PRIMARY, got {other:?}"),
205 }
206 }
207
208 #[test]
209 fn indeterminate_inherits_progress_dimensions() {
210 let p = progress_indeterminate();
211 assert_eq!(p.width, Size::Fill(1.0));
212 assert_eq!(p.height, Size::Fixed(DEFAULT_HEIGHT));
213 }
214
215 #[test]
216 fn layout_fills_proportionally_to_value() {
217 use crate::layout::layout;
218 use crate::state::UiState;
219
220 let mut tree = progress(0.25);
221 let mut state = UiState::new();
222 let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
223 layout(&mut tree, &mut state, viewport);
224 let fill_rect = tree.children[1].computed_rect;
225 assert!(
226 (fill_rect.w - 50.0).abs() < 1e-3,
227 "0.25 * 200 = 50; got {}",
228 fill_rect.w
229 );
230 }
231}