aetna_core/widgets/
progress.rs1use std::panic::Location;
33
34use crate::layout::LayoutCtx;
35use crate::metrics::MetricsRole;
36use crate::shader::{ShaderBinding, StockShader, UniformValue};
37use crate::tokens;
38use crate::tree::*;
39
40pub const DEFAULT_HEIGHT: f32 = 8.0;
42
43#[track_caller]
53pub fn progress(value: f32, fill_color: Color) -> El {
54 let value = value.clamp(0.0, 1.0);
55 let layout = move |ctx: LayoutCtx| {
56 let r = ctx.container;
57 vec![
58 Rect::new(r.x, r.y, r.w, r.h),
60 Rect::new(r.x, r.y, r.w * value, r.h),
62 ]
63 };
64
65 stack([
66 El::new(Kind::Custom("progress-track"))
67 .fill(tokens::MUTED)
68 .radius(tokens::RADIUS_PILL),
69 El::new(Kind::Custom("progress-fill"))
70 .fill(fill_color)
71 .radius(tokens::RADIUS_PILL),
72 ])
73 .at_loc(Location::caller())
74 .metrics_role(MetricsRole::Progress)
75 .layout(layout)
76 .width(Size::Fill(1.0))
77 .default_height(Size::Fixed(DEFAULT_HEIGHT))
78}
79
80#[track_caller]
99pub fn progress_indeterminate(bar_color: Color) -> El {
100 let binding = ShaderBinding::stock(StockShader::ProgressIndeterminate)
101 .with("vec_a", UniformValue::Color(bar_color))
102 .with("vec_b", UniformValue::Color(tokens::MUTED))
103 .with(
108 "vec_c",
109 UniformValue::Vec4([tokens::RADIUS_PILL, 0.0, 0.0, 0.0]),
110 );
111
112 El::new(Kind::Custom("progress-indeterminate"))
113 .at_loc(Location::caller())
114 .shader(binding)
115 .metrics_role(MetricsRole::Progress)
116 .width(Size::Fill(1.0))
117 .default_height(Size::Fixed(DEFAULT_HEIGHT))
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 #[test]
125 fn track_and_fill_use_expected_tokens() {
126 let p = progress(0.5, tokens::PRIMARY);
127 assert_eq!(p.children.len(), 2);
128 assert_eq!(p.children[0].fill, Some(tokens::MUTED), "track is muted");
129 assert_eq!(
130 p.children[1].fill,
131 Some(tokens::PRIMARY),
132 "fill uses caller's color"
133 );
134 assert_eq!(
136 p.children[0].radius,
137 crate::tree::Corners::all(tokens::RADIUS_PILL)
138 );
139 assert_eq!(
140 p.children[1].radius,
141 crate::tree::Corners::all(tokens::RADIUS_PILL)
142 );
143 }
144
145 #[test]
146 fn layout_clamps_value_below_zero() {
147 use crate::layout::layout;
150 use crate::state::UiState;
151
152 let mut tree = progress(-0.5, tokens::PRIMARY);
153 let mut state = UiState::new();
154 let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
155 layout(&mut tree, &mut state, viewport);
156 let fill_rect = state.rect(&tree.children[1].computed_id);
157 assert_eq!(fill_rect.w, 0.0, "negative values clamp to empty fill");
158 }
159
160 #[test]
161 fn layout_clamps_value_above_one() {
162 use crate::layout::layout;
163 use crate::state::UiState;
164
165 let mut tree = progress(1.5, tokens::PRIMARY);
166 let mut state = UiState::new();
167 let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
168 layout(&mut tree, &mut state, viewport);
169 let fill_rect = state.rect(&tree.children[1].computed_id);
170 assert_eq!(fill_rect.w, 200.0, "values above 1.0 clamp to full track");
171 }
172
173 #[test]
174 fn indeterminate_binds_stock_shader() {
175 use crate::shader::ShaderHandle;
176 let p = progress_indeterminate(tokens::PRIMARY);
177 let binding = p.shader_override.as_ref().expect("shader binding");
178 assert_eq!(
179 binding.handle,
180 ShaderHandle::Stock(StockShader::ProgressIndeterminate),
181 "progress_indeterminate must paint through stock::progress_indeterminate",
182 );
183 match binding.uniforms.get("vec_a") {
184 Some(UniformValue::Color(c)) => assert_eq!(*c, tokens::PRIMARY),
185 other => panic!("expected vec_a=PRIMARY, got {other:?}"),
186 }
187 }
188
189 #[test]
190 fn indeterminate_inherits_progress_dimensions() {
191 let p = progress_indeterminate(tokens::PRIMARY);
192 assert_eq!(p.width, Size::Fill(1.0));
193 assert_eq!(p.height, Size::Fixed(DEFAULT_HEIGHT));
194 }
195
196 #[test]
197 fn layout_fills_proportionally_to_value() {
198 use crate::layout::layout;
199 use crate::state::UiState;
200
201 let mut tree = progress(0.25, tokens::PRIMARY);
202 let mut state = UiState::new();
203 let viewport = Rect::new(0.0, 0.0, 200.0, DEFAULT_HEIGHT);
204 layout(&mut tree, &mut state, viewport);
205 let fill_rect = state.rect(&tree.children[1].computed_id);
206 assert!(
207 (fill_rect.w - 50.0).abs() < 1e-3,
208 "0.25 * 200 = 50; got {}",
209 fill_rect.w
210 );
211 }
212}