1use geom::{Circle, Distance, Polygon, Pt2D};
2
3use crate::{
4 Color, Drawable, EdgeInsets, EventCtx, GeomBatch, GfxCtx, Outcome, ScreenDims, ScreenPt,
5 ScreenRectangle, Widget, WidgetImpl, WidgetOutput,
6};
7
8pub struct Slider {
9 current_percent: f64,
10 mouse_on_slider: bool,
11 pub(crate) dragging: bool,
12
13 style: Style,
14 label: Option<String>,
15
16 draw: Drawable,
17
18 top_left: ScreenPt,
19 dims: ScreenDims,
20}
21
22enum Style {
23 Horizontal { main_bg_len: f64, dragger_len: f64 },
24 Vertical { main_bg_len: f64, dragger_len: f64 },
25 Area { width: f64 },
26}
27
28pub const SCROLLBAR_BG_WIDTH: f64 = 8.0;
29pub const AREA_SLIDER_BG_WIDTH: f64 = 10.0;
30
31impl Style {
32 fn padding(&self) -> EdgeInsets {
33 match self {
34 Style::Horizontal { .. } | Style::Vertical { .. } => EdgeInsets::zero(),
35 Style::Area { .. } => EdgeInsets {
36 top: 10.0,
37 bottom: 10.0,
38 left: 20.0,
39 right: 20.0,
40 },
41 }
42 }
43
44 fn inner_dims(&self) -> ScreenDims {
45 match self {
46 Style::Horizontal { main_bg_len, .. } => {
47 ScreenDims::new(*main_bg_len, SCROLLBAR_BG_WIDTH)
48 }
49 Style::Vertical { main_bg_len, .. } => {
50 ScreenDims::new(SCROLLBAR_BG_WIDTH, *main_bg_len)
51 }
52 Style::Area { width } => ScreenDims::new(*width, AREA_SLIDER_BG_WIDTH),
53 }
54 }
55}
56
57impl Slider {
58 pub(crate) fn horizontal_scrollbar(
59 ctx: &EventCtx,
60 width: f64,
61 dragger_len: f64,
62 current_percent: f64,
63 ) -> Widget {
64 Slider::new_widget(
65 ctx,
66 Style::Horizontal {
67 main_bg_len: width,
68 dragger_len,
69 },
70 current_percent,
71 None,
73 )
74 }
75
76 pub(crate) fn vertical_scrollbar(
77 ctx: &EventCtx,
78 height: f64,
79 dragger_len: f64,
80 current_percent: f64,
81 ) -> Widget {
82 Slider::new_widget(
83 ctx,
84 Style::Vertical {
85 main_bg_len: height,
86 dragger_len,
87 },
88 current_percent,
89 None,
91 )
92 }
93
94 pub fn area(ctx: &EventCtx, width: f64, current_percent: f64, label: &str) -> Widget {
95 Slider::new_widget(
96 ctx,
97 Style::Area { width },
98 current_percent,
99 Some(label.to_string()),
100 )
101 .named(label)
102 }
103
104 fn new_widget(
105 ctx: &EventCtx,
106 style: Style,
107 current_percent: f64,
108 label: Option<String>,
109 ) -> Widget {
110 let mut s = Slider {
111 current_percent,
112 mouse_on_slider: false,
113 dragging: false,
114 style,
115 draw: Drawable::empty(ctx),
116 label,
117
118 top_left: ScreenPt::new(0.0, 0.0),
119 dims: ScreenDims::new(0.0, 0.0),
120 };
121 s.recalc(ctx);
122 Widget::new(Box::new(s))
123 }
124
125 fn recalc(&mut self, ctx: &EventCtx) {
126 let mut batch = GeomBatch::new();
127
128 match self.style {
129 Style::Horizontal { .. } | Style::Vertical { .. } => {
130 let inner_dims = self.style.inner_dims();
131 batch.push(
133 ctx.style.field_bg,
134 Polygon::rectangle(inner_dims.width, inner_dims.height),
135 );
136
137 batch.push(
139 if self.mouse_on_slider {
140 ctx.style.btn_solid.bg_hover
141 } else {
142 ctx.style.btn_solid.bg
143 },
144 self.button_geom(),
145 );
146 }
147 Style::Area { .. } => {
148 let inner_dims = self.style.inner_dims();
150 batch.push(
152 ctx.style.field_bg.dull(0.5),
153 Polygon::pill(inner_dims.width, inner_dims.height),
154 );
155
156 batch.push(
158 Color::hex("#F4DF4D"),
159 Polygon::pill(self.current_percent * inner_dims.width, inner_dims.height),
160 );
161
162 batch.push(
164 if self.mouse_on_slider {
165 ctx.style.btn_solid.bg_hover
166 } else {
167 ctx.style.btn_solid.bg_hover.dull(0.2)
171 },
172 self.button_geom(),
173 );
174 }
175 }
176
177 let padding = self.style.padding();
178 batch = batch.translate(padding.left, padding.top);
179 self.dims = self.style.inner_dims().pad(padding);
180 self.draw = ctx.upload(batch);
181 }
182
183 fn button_geom(&self) -> Polygon {
185 match self.style {
186 Style::Horizontal {
187 main_bg_len,
188 dragger_len,
189 } => Polygon::pill(dragger_len, SCROLLBAR_BG_WIDTH)
190 .translate(self.current_percent * (main_bg_len - dragger_len), 0.0),
191 Style::Vertical {
192 main_bg_len,
193 dragger_len,
194 } => Polygon::pill(SCROLLBAR_BG_WIDTH, dragger_len)
195 .translate(0.0, self.current_percent * (main_bg_len - dragger_len)),
196 Style::Area { width } => Circle::new(
197 Pt2D::new(self.current_percent * width, AREA_SLIDER_BG_WIDTH / 2.0),
198 Distance::meters(16.0),
199 )
200 .to_polygon(),
201 }
202 }
203
204 fn pt_to_percent(&self, pt: ScreenPt) -> f64 {
205 let padding = self.style.padding();
206 let pt = pt.translated(
207 -self.top_left.x - padding.left,
208 -self.top_left.y - padding.top,
209 );
210
211 match self.style {
212 Style::Horizontal {
213 main_bg_len,
214 dragger_len,
215 } => (pt.x - (dragger_len / 2.0)) / (main_bg_len - dragger_len),
216 Style::Vertical {
217 main_bg_len,
218 dragger_len,
219 } => (pt.y - (dragger_len / 2.0)) / (main_bg_len - dragger_len),
220 Style::Area { width } => pt.x / width,
221 }
222 }
223
224 pub fn get_percent(&self) -> f64 {
225 self.current_percent
226 }
227
228 pub fn get_value(&self, num_items: usize) -> usize {
229 (self.current_percent * (num_items as f64 - 1.0)) as usize
230 }
231
232 pub fn set_percent(&mut self, ctx: &EventCtx, percent: f64) {
233 assert!((0.0..=1.0).contains(&percent));
234 self.current_percent = percent;
235 self.recalc(ctx);
236 if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
237 self.mouse_on_slider = self
238 .button_geom()
239 .translate(self.top_left.x, self.top_left.y)
240 .contains_pt(pt.to_pt());
241 } else {
242 self.mouse_on_slider = false;
243 }
244 }
245
246 fn inner_event(&mut self, ctx: &mut EventCtx) -> bool {
248 if self.dragging {
249 if ctx.input.get_moved_mouse().is_some() {
250 self.current_percent = self
251 .pt_to_percent(ctx.canvas.get_cursor())
252 .min(1.0)
253 .max(0.0);
254 return true;
255 }
256 if ctx.input.left_mouse_button_released() {
257 self.dragging = false;
258 return true;
259 }
260 return false;
261 }
262 let padding = self.style.padding();
263 if ctx.redo_mouseover() {
264 let old = self.mouse_on_slider;
265 if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
266 self.mouse_on_slider = self
267 .button_geom()
268 .translate(
269 self.top_left.x + padding.left,
270 self.top_left.y + padding.top,
271 )
272 .contains_pt(pt.to_pt());
273 } else {
274 self.mouse_on_slider = false;
275 }
276 return self.mouse_on_slider != old;
277 }
278 if ctx.input.left_mouse_button_pressed() {
279 if self.mouse_on_slider {
280 self.dragging = true;
281 return true;
282 }
283
284 if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
286 if Polygon::rectangle(self.dims.width, self.dims.height)
287 .translate(
288 self.top_left.x + padding.left,
289 self.top_left.y + padding.top,
290 )
291 .contains_pt(pt.to_pt())
292 {
293 self.current_percent = self
294 .pt_to_percent(ctx.canvas.get_cursor())
295 .min(1.0)
296 .max(0.0);
297 self.mouse_on_slider = true;
298 self.dragging = true;
299 return true;
300 }
301 }
302 }
303 false
304 }
305}
306
307impl WidgetImpl for Slider {
308 fn get_dims(&self) -> ScreenDims {
309 self.dims
310 }
311
312 fn set_pos(&mut self, top_left: ScreenPt) {
313 self.top_left = top_left;
314 }
315
316 fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
317 if self.inner_event(ctx) {
318 self.recalc(ctx);
319 if let Some(ref label) = self.label {
320 output.outcome = Outcome::Changed(label.clone());
321 }
322 }
323 }
324
325 fn draw(&self, g: &mut GfxCtx) {
326 g.redraw_at(self.top_left, &self.draw);
327 g.canvas
330 .mark_covered_area(ScreenRectangle::top_left(self.top_left, self.dims));
331 }
332}