1use crate::{
4 backend::{MouseButton, Window, WindowEvent, create_window},
5 error::Error,
6 render::{Canvas, Font},
7 ui::{
8 BASE_BUTTON_HEIGHT, BASE_BUTTON_SPACING, BASE_CORNER_RADIUS, Colors, KEY_END, KEY_ESCAPE,
9 KEY_HOME, KEY_LEFT, KEY_RETURN, KEY_RIGHT,
10 widgets::{Widget, button::Button},
11 },
12};
13
14const BASE_PADDING: u32 = 20;
15const BASE_SLIDER_HEIGHT: u32 = 8;
16const BASE_THUMB_SIZE: u32 = 20;
17const BASE_SLIDER_WIDTH: u32 = 300;
18const BASE_MIN_WIDTH: u32 = 350;
19
20#[derive(Debug, Clone)]
22pub enum ScaleResult {
23 Value(i32),
25 Cancelled,
27 Closed,
29}
30
31impl ScaleResult {
32 pub fn exit_code(&self) -> i32 {
33 match self {
34 ScaleResult::Value(_) => 0,
35 ScaleResult::Cancelled => 1,
36 ScaleResult::Closed => 1,
37 }
38 }
39}
40
41pub struct ScaleBuilder {
43 title: String,
44 text: String,
45 value: i32,
46 min_value: i32,
47 max_value: i32,
48 step: i32,
49 hide_value: bool,
50 width: Option<u32>,
51 height: Option<u32>,
52 colors: Option<&'static Colors>,
53}
54
55impl ScaleBuilder {
56 pub fn new() -> Self {
57 Self {
58 title: String::new(),
59 text: String::new(),
60 value: 0,
61 min_value: 0,
62 max_value: 100,
63 step: 1,
64 hide_value: false,
65 width: None,
66 height: None,
67 colors: None,
68 }
69 }
70
71 pub fn title(mut self, title: &str) -> Self {
72 self.title = title.to_string();
73 self
74 }
75
76 pub fn text(mut self, text: &str) -> Self {
77 self.text = text.to_string();
78 self
79 }
80
81 pub fn value(mut self, value: i32) -> Self {
83 self.value = value;
84 self
85 }
86
87 pub fn min_value(mut self, min: i32) -> Self {
89 self.min_value = min;
90 self
91 }
92
93 pub fn max_value(mut self, max: i32) -> Self {
95 self.max_value = max;
96 self
97 }
98
99 pub fn step(mut self, step: i32) -> Self {
101 self.step = step.max(1);
102 self
103 }
104
105 pub fn hide_value(mut self, hide: bool) -> Self {
107 self.hide_value = hide;
108 self
109 }
110
111 pub fn colors(mut self, colors: &'static Colors) -> Self {
112 self.colors = Some(colors);
113 self
114 }
115
116 pub fn width(mut self, width: u32) -> Self {
117 self.width = Some(width);
118 self
119 }
120
121 pub fn height(mut self, height: u32) -> Self {
122 self.height = Some(height);
123 self
124 }
125
126 pub fn show(self) -> Result<ScaleResult, Error> {
127 let colors = self.colors.unwrap_or_else(|| crate::ui::detect_theme());
128
129 let mut value = self.value.clamp(self.min_value, self.max_value);
131
132 let temp_font = Font::load(1.0);
134 let temp_ok = Button::new("OK", &temp_font, 1.0);
135 let temp_cancel = Button::new("Cancel", &temp_font, 1.0);
136 let temp_prompt_height = if !self.text.is_empty() {
137 temp_font.render(&self.text).finish().height()
138 } else {
139 0
140 };
141
142 let logical_buttons_width = temp_ok.width() + temp_cancel.width() + 10;
143 let logical_content_width = BASE_SLIDER_WIDTH.max(logical_buttons_width);
144 let calc_width = (logical_content_width + BASE_PADDING * 2).max(BASE_MIN_WIDTH);
145
146 let value_display_height = if self.hide_value { 0 } else { 24 };
148 let calc_height = BASE_PADDING * 2
149 + temp_prompt_height
150 + (if temp_prompt_height > 0 { 16 } else { 0 })
151 + BASE_THUMB_SIZE + 16 + value_display_height
153 + 32 + 16; drop(temp_font);
156 drop(temp_ok);
157 drop(temp_cancel);
158
159 let logical_width = self.width.unwrap_or(calc_width) as u16;
161 let logical_height = self.height.unwrap_or(calc_height) as u16;
162
163 let mut window = create_window(logical_width, logical_height)?;
165 window.set_title(if self.title.is_empty() {
166 "Scale"
167 } else {
168 &self.title
169 })?;
170
171 let scale = window.scale_factor();
173
174 let font = Font::load(scale);
176
177 let padding = (BASE_PADDING as f32 * scale) as u32;
179 let slider_height = (BASE_SLIDER_HEIGHT as f32 * scale) as u32;
180 let thumb_size = (BASE_THUMB_SIZE as f32 * scale) as u32;
181 let slider_width = (BASE_SLIDER_WIDTH as f32 * scale) as u32;
182
183 let physical_width = (logical_width as f32 * scale) as u32;
185 let physical_height = (logical_height as f32 * scale) as u32;
186
187 let mut ok_button = Button::new("OK", &font, scale);
189 let mut cancel_button = Button::new("Cancel", &font, scale);
190
191 let prompt_canvas = if !self.text.is_empty() {
193 Some(font.render(&self.text).with_color(colors.text).finish())
194 } else {
195 None
196 };
197 let prompt_height = prompt_canvas.as_ref().map(|c| c.height()).unwrap_or(0);
198
199 let mut y = padding as i32;
201 let prompt_y = y;
202 if prompt_height > 0 {
203 y += prompt_height as i32 + (16.0 * scale) as i32;
204 }
205
206 let slider_x = (physical_width - slider_width) as i32 / 2;
208 let slider_y = y + (thumb_size as i32 - slider_height as i32) / 2;
209 let thumb_y = y;
210 y += thumb_size as i32 + (16.0 * scale) as i32;
211
212 let button_y =
214 physical_height as i32 - padding as i32 - (BASE_BUTTON_HEIGHT as f32 * scale) as i32;
215 let mut button_x = physical_width as i32 - padding as i32;
216 button_x -= cancel_button.width() as i32;
217 cancel_button.set_position(button_x, button_y);
218 button_x -= (BASE_BUTTON_SPACING as f32 * scale) as i32 + ok_button.width() as i32;
219 ok_button.set_position(button_x, button_y);
220
221 let mut dragging = false;
223 let mut thumb_hovered = false;
224 let mut cursor_x = 0i32;
225 let mut cursor_y = 0i32;
226
227 let mut canvas = Canvas::new(physical_width, physical_height);
229
230 let value_to_thumb_x = |val: i32| -> i32 {
232 let range = (self.max_value - self.min_value) as f32;
233 let ratio = if range > 0.0 {
234 (val - self.min_value) as f32 / range
235 } else {
236 0.0
237 };
238 slider_x + (ratio * (slider_width - thumb_size) as f32) as i32
239 };
240
241 let x_to_value = |x: i32| -> i32 {
243 let track_start = slider_x + thumb_size as i32 / 2;
244 let track_end = slider_x + slider_width as i32 - thumb_size as i32 / 2;
245 let track_width = track_end - track_start;
246
247 let ratio = if track_width > 0 {
248 ((x - track_start) as f32 / track_width as f32).clamp(0.0, 1.0)
249 } else {
250 0.0
251 };
252
253 let range = self.max_value - self.min_value;
254 let raw_value = self.min_value + (ratio * range as f32) as i32;
255
256 let steps = (raw_value - self.min_value) / self.step;
258 (self.min_value + steps * self.step).clamp(self.min_value, self.max_value)
259 };
260
261 let draw = |canvas: &mut Canvas,
263 colors: &Colors,
264 font: &Font,
265 prompt_canvas: &Option<Canvas>,
266 value: i32,
267 thumb_hovered: bool,
268 dragging: bool,
269 ok_button: &Button,
270 cancel_button: &Button,
271 hide_value: bool,
272 padding: u32,
274 slider_x: i32,
275 slider_y: i32,
276 slider_width: u32,
277 slider_height: u32,
278 thumb_y: i32,
279 thumb_size: u32,
280 value_y: i32,
281 prompt_y: i32,
282 physical_width: u32,
283 scale: f32,
284 value_to_thumb_x: &dyn Fn(i32) -> i32| {
285 let width = canvas.width() as f32;
286 let height = canvas.height() as f32;
287 let radius = BASE_CORNER_RADIUS * scale;
288
289 canvas.fill_dialog_bg(
290 width,
291 height,
292 colors.window_bg,
293 colors.window_border,
294 colors.window_shadow,
295 radius,
296 );
297
298 if let Some(prompt) = prompt_canvas {
300 canvas.draw_canvas(prompt, padding as i32, prompt_y);
301 }
302
303 canvas.fill_rounded_rect(
305 slider_x as f32,
306 slider_y as f32,
307 slider_width as f32,
308 slider_height as f32,
309 slider_height as f32 / 2.0,
310 colors.progress_bg,
311 );
312
313 let thumb_x = value_to_thumb_x(value);
315 let fill_width = (thumb_x - slider_x + thumb_size as i32 / 2) as f32;
316 if fill_width > 0.0 {
317 canvas.fill_rounded_rect(
318 slider_x as f32,
319 slider_y as f32,
320 fill_width.min(slider_width as f32),
321 slider_height as f32,
322 slider_height as f32 / 2.0,
323 colors.progress_fill,
324 );
325 }
326
327 canvas.stroke_rounded_rect(
329 slider_x as f32,
330 slider_y as f32,
331 slider_width as f32,
332 slider_height as f32,
333 slider_height as f32 / 2.0,
334 colors.progress_border,
335 1.0,
336 );
337
338 let thumb_color = if dragging {
340 colors.button_pressed
341 } else if thumb_hovered {
342 colors.button_hover
343 } else {
344 colors.button
345 };
346 canvas.fill_rounded_rect(
347 thumb_x as f32,
348 thumb_y as f32,
349 thumb_size as f32,
350 thumb_size as f32,
351 thumb_size as f32 / 2.0,
352 thumb_color,
353 );
354 canvas.stroke_rounded_rect(
355 thumb_x as f32,
356 thumb_y as f32,
357 thumb_size as f32,
358 thumb_size as f32,
359 thumb_size as f32 / 2.0,
360 colors.button_outline,
361 1.0,
362 );
363
364 if !hide_value {
366 let value_text = value.to_string();
367 let value_canvas = font.render(&value_text).with_color(colors.text).finish();
368 let value_x = (physical_width - value_canvas.width()) as i32 / 2;
369 canvas.draw_canvas(&value_canvas, value_x, value_y);
370 }
371
372 ok_button.draw_to(canvas, colors, font);
374 cancel_button.draw_to(canvas, colors, font);
375 };
376
377 draw(
379 &mut canvas,
380 colors,
381 &font,
382 &prompt_canvas,
383 value,
384 thumb_hovered,
385 dragging,
386 &ok_button,
387 &cancel_button,
388 self.hide_value,
389 padding,
390 slider_x,
391 slider_y,
392 slider_width,
393 slider_height,
394 thumb_y,
395 thumb_size,
396 y,
397 prompt_y,
398 physical_width,
399 scale,
400 &value_to_thumb_x,
401 );
402 window.set_contents(&canvas)?;
403 window.show()?;
404
405 let mut window_dragging = false;
407 loop {
408 let event = window.wait_for_event()?;
409 let mut needs_redraw = false;
410
411 match &event {
412 WindowEvent::CloseRequested => return Ok(ScaleResult::Closed),
413 WindowEvent::RedrawRequested => needs_redraw = true,
414 WindowEvent::CursorMove(pos) => {
415 if window_dragging {
416 let _ = window.start_drag();
417 window_dragging = false;
418 }
419
420 cursor_x = pos.x as i32;
421 cursor_y = pos.y as i32;
422
423 let thumb_x = value_to_thumb_x(value);
425 let old_hovered = thumb_hovered;
426 thumb_hovered = cursor_x >= thumb_x
427 && cursor_x < thumb_x + thumb_size as i32
428 && cursor_y >= thumb_y
429 && cursor_y < thumb_y + thumb_size as i32;
430
431 if old_hovered != thumb_hovered {
432 needs_redraw = true;
433 }
434
435 if dragging {
437 let new_value = x_to_value(cursor_x);
438 if new_value != value {
439 value = new_value;
440 needs_redraw = true;
441 }
442 }
443 }
444 WindowEvent::ButtonPress(MouseButton::Left, _) => {
445 window_dragging = true;
446 let mx = cursor_x;
447 let my = cursor_y;
448
449 let thumb_x = value_to_thumb_x(value);
451 if mx >= thumb_x
452 && mx < thumb_x + thumb_size as i32
453 && my >= thumb_y
454 && my < thumb_y + thumb_size as i32
455 {
456 dragging = true;
457 needs_redraw = true;
458 }
459 else if mx >= slider_x
461 && mx < slider_x + slider_width as i32
462 && my >= slider_y
463 && my < slider_y + slider_height as i32 + thumb_size as i32
464 {
465 let new_value = x_to_value(mx);
466 if new_value != value {
467 value = new_value;
468 needs_redraw = true;
469 }
470 dragging = true;
471 }
472 }
473 WindowEvent::ButtonRelease(MouseButton::Left, _) => {
474 window_dragging = false;
475 if dragging {
476 dragging = false;
477 needs_redraw = true;
478 }
479 }
480 WindowEvent::KeyPress(key_event) => {
481 match key_event.keysym {
482 KEY_LEFT => {
483 let new_value = (value - self.step).max(self.min_value);
484 if new_value != value {
485 value = new_value;
486 needs_redraw = true;
487 }
488 }
489 KEY_RIGHT => {
490 let new_value = (value + self.step).min(self.max_value);
491 if new_value != value {
492 value = new_value;
493 needs_redraw = true;
494 }
495 }
496 KEY_HOME => {
497 if value != self.min_value {
498 value = self.min_value;
499 needs_redraw = true;
500 }
501 }
502 KEY_END => {
503 if value != self.max_value {
504 value = self.max_value;
505 needs_redraw = true;
506 }
507 }
508 KEY_RETURN => {
509 return Ok(ScaleResult::Value(value));
510 }
511 KEY_ESCAPE => {
512 return Ok(ScaleResult::Cancelled);
513 }
514 _ => {}
515 }
516 }
517 _ => {}
518 }
519
520 needs_redraw |= ok_button.process_event(&event);
521 needs_redraw |= cancel_button.process_event(&event);
522
523 if ok_button.was_clicked() {
524 return Ok(ScaleResult::Value(value));
525 }
526 if cancel_button.was_clicked() {
527 return Ok(ScaleResult::Cancelled);
528 }
529
530 while let Some(ev) = window.poll_for_event()? {
532 match &ev {
533 WindowEvent::CloseRequested => return Ok(ScaleResult::Closed),
534 WindowEvent::CursorMove(pos) if dragging => {
535 let new_value = x_to_value(pos.x as i32);
536 if new_value != value {
537 value = new_value;
538 needs_redraw = true;
539 }
540 }
541 WindowEvent::ButtonRelease(MouseButton::Left, _) => {
542 if dragging {
543 dragging = false;
544 needs_redraw = true;
545 }
546 }
547 _ => {}
548 }
549 needs_redraw |= ok_button.process_event(&ev);
550 needs_redraw |= cancel_button.process_event(&ev);
551 }
552
553 if needs_redraw {
554 draw(
555 &mut canvas,
556 colors,
557 &font,
558 &prompt_canvas,
559 value,
560 thumb_hovered,
561 dragging,
562 &ok_button,
563 &cancel_button,
564 self.hide_value,
565 padding,
566 slider_x,
567 slider_y,
568 slider_width,
569 slider_height,
570 thumb_y,
571 thumb_size,
572 y,
573 prompt_y,
574 physical_width,
575 scale,
576 &value_to_thumb_x,
577 );
578 window.set_contents(&canvas)?;
579 }
580 }
581 }
582}
583
584impl Default for ScaleBuilder {
585 fn default() -> Self {
586 Self::new()
587 }
588}