Skip to main content

zenity_rs/ui/
scale.rs

1//! Scale dialog implementation for selecting a numeric value with a slider.
2
3use 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/// Scale dialog result.
21#[derive(Debug, Clone)]
22pub enum ScaleResult {
23    /// User selected a value and clicked OK.
24    Value(i32),
25    /// User cancelled the dialog.
26    Cancelled,
27    /// Dialog was closed.
28    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
41/// Scale dialog builder.
42pub 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    /// Set the initial value.
82    pub fn value(mut self, value: i32) -> Self {
83        self.value = value;
84        self
85    }
86
87    /// Set the minimum value (default: 0).
88    pub fn min_value(mut self, min: i32) -> Self {
89        self.min_value = min;
90        self
91    }
92
93    /// Set the maximum value (default: 100).
94    pub fn max_value(mut self, max: i32) -> Self {
95        self.max_value = max;
96        self
97    }
98
99    /// Set the step increment (default: 1).
100    pub fn step(mut self, step: i32) -> Self {
101        self.step = step.max(1);
102        self
103    }
104
105    /// Hide the value display.
106    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        // Clamp initial value to range
130        let mut value = self.value.clamp(self.min_value, self.max_value);
131
132        // First pass: calculate LOGICAL dimensions using scale 1.0
133        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        // Height: padding + text + slider area + value display + buttons + padding
147        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  // Slider area with some margin
152            + value_display_height
153            + 32 + 16; // Buttons
154
155        drop(temp_font);
156        drop(temp_ok);
157        drop(temp_cancel);
158
159        // Use custom dimensions if provided, otherwise use calculated defaults
160        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        // Create window with LOGICAL dimensions
164        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        // Get the actual scale factor from the window (compositor scale)
172        let scale = window.scale_factor();
173
174        // Now create everything at PHYSICAL scale
175        let font = Font::load(scale);
176
177        // Scale dimensions for physical rendering
178        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        // Calculate physical dimensions
184        let physical_width = (logical_width as f32 * scale) as u32;
185        let physical_height = (logical_height as f32 * scale) as u32;
186
187        // Create buttons at physical scale
188        let mut ok_button = Button::new("OK", &font, scale);
189        let mut cancel_button = Button::new("Cancel", &font, scale);
190
191        // Render prompt text at physical scale
192        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        // Layout calculation
200        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        // Slider position (centered horizontally)
207        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        // Button positions (right-aligned)
213        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        // State
222        let mut dragging = false;
223        let mut thumb_hovered = false;
224        let mut cursor_x = 0i32;
225        let mut cursor_y = 0i32;
226
227        // Create canvas at PHYSICAL dimensions
228        let mut canvas = Canvas::new(physical_width, physical_height);
229
230        // Helper to calculate thumb position from value
231        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        // Helper to calculate value from x position
242        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            // Snap to step
257            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        // Draw function
262        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                    // Layout params
273                    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            // Draw prompt
299            if let Some(prompt) = prompt_canvas {
300                canvas.draw_canvas(prompt, padding as i32, prompt_y);
301            }
302
303            // Draw slider track background
304            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            // Draw filled portion of track
314            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            // Draw track border
328            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            // Draw thumb
339            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            // Draw value display
365            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            // Draw buttons
373            ok_button.draw_to(canvas, colors, font);
374            cancel_button.draw_to(canvas, colors, font);
375        };
376
377        // Initial draw
378        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        // Event loop
406        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                    // Check thumb hover
424                    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                    // Handle dragging
436                    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                    // Check if clicking on thumb
450                    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                    // Check if clicking on track
460                    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            // Batch process pending events
531            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}