1use std::ops;
2
3use geom::{trim_f64, CornerRadii, Distance, Polygon, Pt2D};
4
5use crate::{
6 include_labeled_bytes, Button, Drawable, EdgeInsets, EventCtx, GeomBatch, GfxCtx, Outcome,
7 OutlineStyle, Prerender, ScreenDims, ScreenPt, ScreenRectangle, Style, Text, Widget,
8 WidgetImpl, WidgetOutput,
9};
10
11const TEXT_WIDTH: f64 = 100.0;
13
14pub trait SpinnerValue:
15 Copy
16 + PartialOrd
17 + std::fmt::Display
18 + std::ops::Add<Output = Self>
19 + std::ops::AddAssign
20 + std::ops::Sub<Output = Self>
21 + std::ops::SubAssign
22where
23 Self: std::marker::Sized,
24{
25}
26
27impl<T> SpinnerValue for T where
28 T: Copy
29 + PartialOrd
30 + std::fmt::Display
31 + std::ops::Add<Output = Self>
32 + std::ops::AddAssign
33 + std::ops::Sub<Output = Self>
34 + std::ops::SubAssign
35{
36}
37
38pub struct Spinner<T> {
42 low: T,
43 high: T,
44 step_size: T,
45 pub current: T,
46 label: String,
47 render_value: Box<dyn Fn(T) -> String>,
48
49 up: Button,
50 down: Button,
51 outline: OutlineStyle,
52 drawable: Drawable,
53
54 top_left: ScreenPt,
55 dims: ScreenDims,
56}
57
58impl<T: 'static + SpinnerValue> Spinner<T> {
59 pub fn widget(
62 ctx: &EventCtx,
63 label: impl Into<String>,
64 (low, high): (T, T),
65 current: T,
66 step_size: T,
67 ) -> Widget {
68 Spinner::widget_with_custom_rendering(
69 ctx,
70 label,
71 (low, high),
72 current,
73 step_size,
74 Box::new(|x| x.to_string()),
75 )
76 }
77
78 pub fn widget_with_custom_rendering(
80 ctx: &EventCtx,
81 label: impl Into<String>,
82 (low, high): (T, T),
83 current: T,
84 step_size: T,
85 render_value: Box<dyn Fn(T) -> String>,
86 ) -> Widget {
87 let label = label.into();
88 Widget::new(Box::new(Self::new(
89 ctx,
90 label.clone(),
91 (low, high),
92 current,
93 step_size,
94 render_value,
95 )))
96 .named(label)
97 }
98
99 fn new(
100 ctx: &EventCtx,
101 label: String,
102 (low, high): (T, T),
103 mut current: T,
104 step_size: T,
105 render_value: Box<dyn Fn(T) -> String>,
106 ) -> Self {
107 let button_builder = ctx
108 .style()
109 .btn_plain
110 .btn()
111 .padding(EdgeInsets {
112 top: 2.0,
113 bottom: 2.0,
114 left: 4.0,
115 right: 4.0,
116 })
117 .image_dims(17.0);
118
119 let up = button_builder
120 .clone()
121 .image_bytes(include_labeled_bytes!("../../icons/arrow_up.svg"))
122 .corner_rounding(CornerRadii {
123 top_left: 0.0,
124 top_right: 5.0,
125 bottom_right: 0.0,
126 bottom_left: 5.0,
127 })
128 .build(ctx, "increase value");
129
130 let down = button_builder
131 .image_bytes(include_labeled_bytes!("../../icons/arrow_down.svg"))
132 .corner_rounding(CornerRadii {
133 top_left: 5.0,
134 top_right: 0.0,
135 bottom_right: 5.0,
136 bottom_left: 0.0,
137 })
138 .build(ctx, "decrease value");
139
140 let outline = ctx.style().btn_outline.outline;
141 let dims = ScreenDims::new(
142 TEXT_WIDTH + up.get_dims().width,
143 up.get_dims().height + down.get_dims().height + 1.0,
144 );
145 if current < low {
146 warn!(
147 "Spinner's initial value is out of bounds! {}, bounds ({}, {})",
148 current, low, high
149 );
150 current = low;
151 } else if high < current {
152 warn!(
153 "Spinner's initial value is out of bounds! {}, bounds ({}, {})",
154 current, low, high
155 );
156 current = high;
157 }
158
159 let mut spinner = Spinner {
160 low,
161 high,
162 current,
163 step_size,
164 label,
165 render_value,
166
167 up,
168 down,
169 drawable: Drawable::empty(ctx),
170 outline,
171 top_left: ScreenPt::new(0.0, 0.0),
172 dims,
173 };
174 spinner.drawable = spinner.drawable(ctx.prerender, ctx.style());
175 spinner
176 }
177
178 pub fn modify(&mut self, ctx: &EventCtx, delta: T) {
179 self.current += delta;
180 self.clamp();
181 self.drawable = self.drawable(ctx.prerender, ctx.style());
182 }
183
184 fn clamp(&mut self) {
185 if self.current > self.high {
186 self.current = self.high;
187 }
188 if self.current < self.low {
189 self.current = self.low;
190 }
191 }
192
193 fn drawable(&self, prerender: &Prerender, style: &Style) -> Drawable {
194 let mut batch = GeomBatch::from(vec![(
195 style.field_bg,
196 Polygon::rounded_rectangle(self.dims.width, self.dims.height, 5.0),
197 )]);
198 batch.append(
199 Text::from((self.render_value)(self.current))
200 .render_autocropped(prerender)
201 .centered_on(Pt2D::new(TEXT_WIDTH / 2.0, self.dims.height / 2.0)),
202 );
203 batch.push(
204 self.outline.1,
205 Polygon::rounded_rectangle(self.dims.width, self.dims.height, 5.0)
206 .to_outline(Distance::meters(self.outline.0)),
207 );
208 prerender.upload(batch)
209 }
210}
211
212impl<T: 'static + SpinnerValue> WidgetImpl for Spinner<T> {
213 fn get_dims(&self) -> ScreenDims {
214 self.dims
215 }
216
217 fn set_pos(&mut self, top_left: ScreenPt) {
218 self.top_left = top_left;
221 self.up
222 .set_pos(ScreenPt::new(top_left.x + TEXT_WIDTH, top_left.y));
223 self.down.set_pos(ScreenPt::new(
224 top_left.x + TEXT_WIDTH,
225 top_left.y + self.up.get_dims().height,
226 ));
227 }
228
229 fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
230 self.up.event(ctx, output);
231 if let Outcome::Clicked(_) = output.outcome {
232 output.outcome = Outcome::Changed(self.label.clone());
233 self.current += self.step_size;
234 self.clamp();
235 self.drawable = self.drawable(ctx.prerender, ctx.style());
236 ctx.no_op_event(true, |ctx| self.up.event(ctx, output));
237 return;
238 }
239
240 self.down.event(ctx, output);
241 if let Outcome::Clicked(_) = output.outcome {
242 output.outcome = Outcome::Changed(self.label.clone());
243 self.current -= self.step_size;
244 self.clamp();
245 self.drawable = self.drawable(ctx.prerender, ctx.style());
246 ctx.no_op_event(true, |ctx| self.down.event(ctx, output));
247 return;
248 }
249
250 if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
251 if ScreenRectangle::top_left(self.top_left, self.dims).contains(pt) {
252 if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
253 if dy > 0.0 && self.current < self.high {
254 self.current += self.step_size;
255 self.clamp();
256 output.outcome = Outcome::Changed(self.label.clone());
257 self.drawable = self.drawable(ctx.prerender, ctx.style());
258 }
259 if dy < 0.0 && self.current > self.low {
260 self.current -= self.step_size;
261 self.clamp();
262 output.outcome = Outcome::Changed(self.label.clone());
263 self.drawable = self.drawable(ctx.prerender, ctx.style());
264 }
265 }
266 }
267 }
268 }
269
270 fn draw(&self, g: &mut GfxCtx) {
271 g.redraw_at(self.top_left, &self.drawable);
272
273 self.up.draw(g);
274 self.down.draw(g);
275 }
276
277 fn can_restore(&self) -> bool {
278 true
279 }
280 fn restore(&mut self, ctx: &mut EventCtx, prev: &dyn WidgetImpl) {
281 let prev = prev.downcast_ref::<Spinner<T>>().unwrap();
282 self.current = prev.current;
283 self.drawable = self.drawable(ctx.prerender, ctx.style());
284 }
285}
286
287#[derive(Clone, Copy, PartialEq, PartialOrd)]
290pub struct RoundedF64(pub f64);
291
292impl ops::Add for RoundedF64 {
293 type Output = RoundedF64;
294
295 fn add(self, other: RoundedF64) -> RoundedF64 {
296 RoundedF64(trim_f64(self.0 + other.0))
297 }
298}
299
300impl ops::AddAssign for RoundedF64 {
301 fn add_assign(&mut self, other: RoundedF64) {
302 *self = *self + other;
303 }
304}
305
306impl ops::Sub for RoundedF64 {
307 type Output = RoundedF64;
308
309 fn sub(self, other: RoundedF64) -> RoundedF64 {
310 RoundedF64(trim_f64(self.0 - other.0))
311 }
312}
313
314impl ops::SubAssign for RoundedF64 {
315 fn sub_assign(&mut self, other: RoundedF64) {
316 *self = *self - other;
317 }
318}
319
320impl std::fmt::Display for RoundedF64 {
321 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
322 write!(f, "{}", self.0)
323 }
324}
325
326impl Spinner<RoundedF64> {
327 pub fn f64_widget(
329 ctx: &EventCtx,
330 label: impl Into<String>,
331 (low, high): (f64, f64),
332 current: f64,
333 step_size: f64,
334 ) -> Widget {
335 Spinner::widget(
336 ctx,
337 label,
338 (RoundedF64(low), RoundedF64(high)),
339 RoundedF64(current),
340 RoundedF64(step_size),
341 )
342 }
343}