Skip to main content

zenity_rs/ui/
text_info.rs

1//! Text info dialog implementation for displaying text from files or stdin.
2
3use std::io::Read;
4
5use crate::{
6    backend::{Window, WindowEvent, create_window},
7    error::Error,
8    render::{Canvas, Font, rgb},
9    ui::{
10        BASE_BUTTON_HEIGHT, BASE_BUTTON_SPACING, BASE_CORNER_RADIUS, Colors, KEY_DOWN, KEY_END,
11        KEY_ESCAPE, KEY_HOME, KEY_PAGE_DOWN, KEY_PAGE_UP, KEY_RETURN, KEY_UP,
12        widgets::{Widget, button::Button},
13    },
14};
15
16const BASE_PADDING: u32 = 16;
17const BASE_LINE_HEIGHT: u32 = 20;
18const BASE_CHECKBOX_SIZE: u32 = 16;
19const BASE_MIN_WIDTH: u32 = 400;
20const BASE_MIN_HEIGHT: u32 = 300;
21const BASE_DEFAULT_WIDTH: u32 = 500;
22const BASE_DEFAULT_HEIGHT: u32 = 400;
23
24/// Text info dialog result.
25#[derive(Debug, Clone)]
26pub enum TextInfoResult {
27    /// User clicked OK. Contains whether checkbox was checked (if present).
28    Ok { checkbox_checked: bool },
29    /// User cancelled the dialog.
30    Cancelled,
31    /// Dialog was closed.
32    Closed,
33}
34
35impl TextInfoResult {
36    pub fn exit_code(&self) -> i32 {
37        match self {
38            TextInfoResult::Ok {
39                checkbox_checked,
40            } => {
41                if *checkbox_checked {
42                    0
43                } else {
44                    1
45                }
46            }
47            TextInfoResult::Cancelled => 1,
48            TextInfoResult::Closed => 1,
49        }
50    }
51}
52
53/// Text info dialog builder.
54pub struct TextInfoBuilder {
55    title: String,
56    filename: Option<String>,
57    checkbox_text: Option<String>,
58    width: Option<u32>,
59    height: Option<u32>,
60    colors: Option<&'static Colors>,
61}
62
63impl TextInfoBuilder {
64    pub fn new() -> Self {
65        Self {
66            title: String::new(),
67            filename: None,
68            checkbox_text: None,
69            width: None,
70            height: None,
71            colors: None,
72        }
73    }
74
75    pub fn title(mut self, title: &str) -> Self {
76        self.title = title.to_string();
77        self
78    }
79
80    /// Set the filename to read text from. If not set, reads from stdin.
81    pub fn filename(mut self, filename: &str) -> Self {
82        self.filename = Some(filename.to_string());
83        self
84    }
85
86    /// Add a checkbox at the bottom (e.g., "I agree to the terms").
87    pub fn checkbox(mut self, text: &str) -> Self {
88        self.checkbox_text = Some(text.to_string());
89        self
90    }
91
92    pub fn colors(mut self, colors: &'static Colors) -> Self {
93        self.colors = Some(colors);
94        self
95    }
96
97    pub fn width(mut self, width: u32) -> Self {
98        self.width = Some(width);
99        self
100    }
101
102    pub fn height(mut self, height: u32) -> Self {
103        self.height = Some(height);
104        self
105    }
106
107    pub fn show(self) -> Result<TextInfoResult, Error> {
108        let colors = self.colors.unwrap_or_else(|| crate::ui::detect_theme());
109
110        // Read content from file or stdin
111        let content = if let Some(ref filename) = self.filename {
112            std::fs::read_to_string(filename).map_err(Error::Io)?
113        } else {
114            let mut buf = String::new();
115            std::io::stdin()
116                .read_to_string(&mut buf)
117                .map_err(Error::Io)?;
118            buf
119        };
120
121        let has_checkbox = self.checkbox_text.is_some();
122
123        // Use provided dimensions or defaults
124        let logical_width = self.width.unwrap_or(BASE_DEFAULT_WIDTH).max(BASE_MIN_WIDTH);
125        let logical_height = self
126            .height
127            .unwrap_or(BASE_DEFAULT_HEIGHT)
128            .max(BASE_MIN_HEIGHT);
129
130        // Create window with LOGICAL dimensions
131        let mut window = create_window(logical_width as u16, logical_height as u16)?;
132        window.set_title(if self.title.is_empty() {
133            "Text"
134        } else {
135            &self.title
136        })?;
137
138        // Get the actual scale factor from the window (compositor scale)
139        let scale = window.scale_factor();
140
141        // Now create everything at PHYSICAL scale
142        let font = Font::load(scale);
143
144        // Scale dimensions for physical rendering
145        let padding = (BASE_PADDING as f32 * scale) as u32;
146        let line_height = (BASE_LINE_HEIGHT as f32 * scale) as u32;
147        let checkbox_size = (BASE_CHECKBOX_SIZE as f32 * scale) as u32;
148
149        // Calculate physical dimensions
150        let physical_width = (logical_width as f32 * scale) as u32;
151        let physical_height = (logical_height as f32 * scale) as u32;
152
153        // Create buttons at physical scale
154        let mut ok_button = Button::new("OK", &font, scale);
155        let mut cancel_button = Button::new("Cancel", &font, scale);
156
157        // Layout calculation
158        let title_height = if self.title.is_empty() {
159            0
160        } else {
161            line_height + (8.0 * scale) as u32
162        };
163        let button_height = (BASE_BUTTON_HEIGHT as f32 * scale) as u32;
164        let checkbox_row_height = if has_checkbox {
165            checkbox_size + (8.0 * scale) as u32
166        } else {
167            0
168        };
169        let button_spacing = (24.0 * scale) as u32;
170        let button_y = (physical_height - padding - button_height) as i32;
171        let checkbox_y = if has_checkbox {
172            button_y - checkbox_row_height as i32 - (8.0 * scale) as i32
173        } else {
174            button_y
175        };
176
177        // Text area bounds (with more spacing below it)
178        let text_area_x = padding as i32;
179        let text_area_y = padding as i32 + title_height as i32;
180        let text_area_w = physical_width - padding * 2;
181        let text_area_bottom = if has_checkbox {
182            checkbox_y as u32 - button_spacing
183        } else {
184            button_y as u32 - button_spacing
185        };
186        let text_area_h = text_area_bottom - padding - (8.0 * scale) as u32;
187
188        // Calculate text wrapping - split content into wrapped lines
189        let max_text_width = text_area_w - (16.0 * scale) as u32; // Account for scrollbar
190        let mut wrapped_lines: Vec<String> = Vec::new();
191
192        for line in content.lines() {
193            if line.is_empty() {
194                wrapped_lines.push(String::new());
195            } else {
196                // Wrap long lines
197                let mut remaining = line;
198                while !remaining.is_empty() {
199                    let (line_w, _) = font.render(remaining).measure();
200                    if line_w as u32 <= max_text_width {
201                        wrapped_lines.push(remaining.to_string());
202                        break;
203                    }
204
205                    // Find break point
206                    let mut break_at = remaining.len();
207                    for (i, _) in remaining.char_indices().rev() {
208                        let test = &remaining[..i];
209                        let (w, _) = font.render(test).measure();
210                        if w as u32 <= max_text_width {
211                            // Try to break at word boundary
212                            if let Some(space_pos) = test.rfind(|c: char| c.is_whitespace()) {
213                                break_at = space_pos + 1;
214                            } else {
215                                break_at = i;
216                            }
217                            break;
218                        }
219                    }
220
221                    if break_at == 0 {
222                        break_at = 1; // Ensure progress
223                    }
224
225                    wrapped_lines.push(remaining[..break_at].trim_end().to_string());
226                    remaining = remaining[break_at..].trim_start();
227                }
228            }
229        }
230
231        let total_lines = wrapped_lines.len();
232        let visible_lines = (text_area_h / line_height) as usize;
233
234        // Button positions (right-aligned)
235        let mut bx = physical_width as i32 - padding as i32;
236        bx -= cancel_button.width() as i32;
237        cancel_button.set_position(bx, button_y);
238        bx -= (BASE_BUTTON_SPACING as f32 * scale) as i32 + ok_button.width() as i32;
239        ok_button.set_position(bx, button_y);
240
241        // State
242        let mut scroll_offset = 0usize;
243        let mut checkbox_checked = false;
244        let mut checkbox_hovered = false;
245        let mut scrollbar_hovered = false;
246
247        // Create canvas at PHYSICAL dimensions
248        let mut canvas = Canvas::new(physical_width, physical_height);
249
250        // Draw function
251        let draw = |canvas: &mut Canvas,
252                    colors: &Colors,
253                    font: &Font,
254                    title: &str,
255                    wrapped_lines: &[String],
256                    scroll_offset: usize,
257                    visible_lines: usize,
258                    checkbox_text: &Option<String>,
259                    checkbox_checked: bool,
260                    checkbox_hovered: bool,
261                    ok_button: &Button,
262                    cancel_button: &Button,
263                    // Scaled parameters
264                    padding: u32,
265                    line_height: u32,
266                    checkbox_size: u32,
267                    text_area_x: i32,
268                    text_area_y: i32,
269                    text_area_w: u32,
270                    text_area_h: u32,
271                    checkbox_y: i32,
272                    scale: f32,
273                    scrollbar_hovered: bool| {
274            let width = canvas.width() as f32;
275            let height = canvas.height() as f32;
276            let radius = BASE_CORNER_RADIUS * scale;
277
278            canvas.fill_dialog_bg(
279                width,
280                height,
281                colors.window_bg,
282                colors.window_border,
283                colors.window_shadow,
284                radius,
285            );
286
287            // Draw title if present
288            if !title.is_empty() {
289                // Render title with larger font (1.5x normal size)
290                let title_font_size = 18.0 * 1.5 * scale;
291                let title_font = Font::load_with_size(title_font_size);
292                let title_rendered = title_font.render(title).with_color(colors.text).finish();
293                let title_x = (width as i32 - title_rendered.width() as i32) / 2;
294                let title_y = padding as i32;
295                canvas.draw_canvas(&title_rendered, title_x, title_y);
296            }
297
298            // Text area background
299            canvas.fill_rounded_rect(
300                text_area_x as f32,
301                text_area_y as f32,
302                text_area_w as f32,
303                text_area_h as f32,
304                6.0 * scale,
305                colors.input_bg,
306            );
307
308            // Draw visible lines
309            let text_padding = (8.0 * scale) as i32;
310            for (i, line_idx) in
311                (scroll_offset..wrapped_lines.len().min(scroll_offset + visible_lines)).enumerate()
312            {
313                let line = &wrapped_lines[line_idx];
314                if !line.is_empty() {
315                    let tc = font.render(line).with_color(colors.text).finish();
316                    let y = text_area_y + text_padding + (i as u32 * line_height) as i32;
317                    canvas.draw_canvas(&tc, text_area_x + text_padding, y);
318                }
319            }
320
321            // Scrollbar
322            if wrapped_lines.len() > visible_lines {
323                let scrollbar_width = if scrollbar_hovered {
324                    12.0 * scale
325                } else {
326                    8.0 * scale
327                };
328                let sb_x = text_area_x + text_area_w as i32 - scrollbar_width as i32;
329                let sb_y = text_area_y as f32 + 4.0 * scale;
330                let sb_h = text_area_h as f32 - 8.0 * scale;
331                let thumb_h =
332                    (visible_lines as f32 / wrapped_lines.len() as f32 * sb_h).max(20.0 * scale);
333                let max_scroll = wrapped_lines.len().saturating_sub(visible_lines);
334                let thumb_y = if max_scroll > 0 {
335                    scroll_offset as f32 / max_scroll as f32 * (sb_h - thumb_h)
336                } else {
337                    0.0
338                };
339
340                // Track
341                canvas.fill_rounded_rect(
342                    sb_x as f32,
343                    sb_y,
344                    scrollbar_width - 2.0 * scale,
345                    sb_h,
346                    3.0 * scale,
347                    darken(colors.input_bg, 0.05),
348                );
349                // Thumb
350                canvas.fill_rounded_rect(
351                    sb_x as f32,
352                    sb_y + thumb_y,
353                    scrollbar_width - 2.0 * scale,
354                    thumb_h,
355                    3.0 * scale,
356                    if scrollbar_hovered {
357                        colors.input_border_focused
358                    } else {
359                        colors.input_border
360                    },
361                );
362            }
363
364            // Border
365            canvas.stroke_rounded_rect(
366                text_area_x as f32,
367                text_area_y as f32,
368                text_area_w as f32,
369                text_area_h as f32,
370                6.0 * scale,
371                colors.input_border,
372                1.0,
373            );
374
375            // Checkbox
376            if let Some(cb_text) = checkbox_text {
377                let cb_x = padding as i32;
378                let cb_y = checkbox_y;
379
380                // Checkbox box
381                let cb_bg = if checkbox_hovered {
382                    darken(colors.input_bg, 0.06)
383                } else {
384                    colors.input_bg
385                };
386                canvas.fill_rounded_rect(
387                    cb_x as f32,
388                    cb_y as f32,
389                    checkbox_size as f32,
390                    checkbox_size as f32,
391                    3.0 * scale,
392                    cb_bg,
393                );
394                canvas.stroke_rounded_rect(
395                    cb_x as f32,
396                    cb_y as f32,
397                    checkbox_size as f32,
398                    checkbox_size as f32,
399                    3.0 * scale,
400                    colors.input_border,
401                    1.0,
402                );
403
404                // Check mark
405                if checkbox_checked {
406                    let inset = (3.0 * scale) as i32;
407                    canvas.fill_rounded_rect(
408                        (cb_x + inset) as f32,
409                        (cb_y + inset) as f32,
410                        (checkbox_size as i32 - inset * 2) as f32,
411                        (checkbox_size as i32 - inset * 2) as f32,
412                        2.0 * scale,
413                        colors.input_border_focused,
414                    );
415                }
416
417                // Label
418                let label_x = cb_x + checkbox_size as i32 + (8.0 * scale) as i32;
419                let tc = font.render(cb_text).with_color(colors.text).finish();
420                canvas.draw_canvas(&tc, label_x, cb_y);
421            }
422
423            // Buttons
424            ok_button.draw_to(canvas, colors, font);
425            cancel_button.draw_to(canvas, colors, font);
426        };
427
428        let mut window_dragging = false;
429
430        // Scrollbar thumb dragging state
431        let mut thumb_drag = false;
432        let mut thumb_drag_offset: Option<i32> = None;
433        let mut last_cursor_pos: Option<(i32, i32)> = None;
434        let mut clicking_scrollbar: bool;
435
436        // Initial draw
437        draw(
438            &mut canvas,
439            colors,
440            &font,
441            &self.title,
442            &wrapped_lines,
443            scroll_offset,
444            visible_lines,
445            &self.checkbox_text,
446            checkbox_checked,
447            checkbox_hovered,
448            &ok_button,
449            &cancel_button,
450            padding,
451            line_height,
452            checkbox_size,
453            text_area_x,
454            text_area_y,
455            text_area_w,
456            text_area_h,
457            checkbox_y,
458            scale,
459            scrollbar_hovered,
460        );
461        window.set_contents(&canvas)?;
462        window.show()?;
463
464        // Event loop
465        loop {
466            let event = window.wait_for_event()?;
467            let mut needs_redraw = false;
468
469            match &event {
470                WindowEvent::CloseRequested => return Ok(TextInfoResult::Closed),
471                WindowEvent::RedrawRequested => needs_redraw = true,
472                WindowEvent::CursorEnter(pos) | WindowEvent::CursorMove(pos) => {
473                    if window_dragging {
474                        let _ = window.start_drag();
475                        window_dragging = false;
476                    }
477
478                    let mx = pos.x as i32;
479                    let my = pos.y as i32;
480
481                    // Store current cursor position
482                    last_cursor_pos = Some((mx, my));
483
484                    // Handle scrollbar thumb dragging
485                    if thumb_drag && total_lines > visible_lines {
486                        let text_area_my = my - text_area_y;
487
488                        let sb_y_f32 = 4.0 * scale;
489                        let sb_y = sb_y_f32 as i32;
490                        let sb_h_f32 = text_area_h as f32 - 8.0 * scale;
491                        let sb_h = sb_h_f32 as i32;
492
493                        let max_scroll = total_lines.saturating_sub(visible_lines);
494                        if max_scroll > 0 {
495                            let thumb_h_f32 = (visible_lines as f32 / total_lines as f32
496                                * sb_h_f32)
497                                .max(20.0 * scale);
498                            let thumb_h = thumb_h_f32 as i32;
499                            let max_thumb_y = sb_h - thumb_h;
500
501                            let offset = thumb_drag_offset.unwrap_or(thumb_h / 2);
502                            let thumb_y = (text_area_my - sb_y - offset).clamp(0, max_thumb_y);
503                            let scroll_ratio = if max_thumb_y > 0 {
504                                thumb_y as f32 / max_thumb_y as f32
505                            } else {
506                                0.0
507                            };
508                            scroll_offset =
509                                ((scroll_ratio * max_scroll as f32) as usize).clamp(0, max_scroll);
510                            needs_redraw = true;
511                        }
512                    } else {
513                        // Update scrollbar hover state (always, not just when there's a checkbox)
514                        let scrollbar_width = if scrollbar_hovered {
515                            12.0 * scale
516                        } else {
517                            8.0 * scale
518                        };
519                        let scrollbar_x = text_area_x + text_area_w as i32 - scrollbar_width as i32;
520
521                        scrollbar_hovered = total_lines > visible_lines
522                            && mx >= scrollbar_x
523                            && mx < text_area_x + text_area_w as i32
524                            && my >= text_area_y
525                            && my < text_area_y + text_area_h as i32;
526
527                        if has_checkbox {
528                            // Check if hovering checkbox area (only if not over scrollbar)
529                            let cb_x = padding as i32;
530                            let cb_row_width = checkbox_size as i32 + (8.0 * scale) as i32 + 200; // Approximate label width
531                            let old_hovered = checkbox_hovered;
532                            checkbox_hovered = !scrollbar_hovered
533                                && mx >= cb_x
534                                && mx < cb_x + cb_row_width
535                                && my >= checkbox_y
536                                && my < checkbox_y + checkbox_size as i32;
537
538                            if old_hovered != checkbox_hovered {
539                                needs_redraw = true;
540                            }
541                        }
542                    }
543                }
544                WindowEvent::ButtonPress(crate::backend::MouseButton::Left, _) => {
545                    window_dragging = true;
546                    clicking_scrollbar = false;
547
548                    // Check if clicking anywhere in scrollbar area (thumb OR track)
549                    if let Some((mx, my)) = last_cursor_pos {
550                        if total_lines > visible_lines {
551                            let scrollbar_width = if scrollbar_hovered {
552                                12.0 * scale
553                            } else {
554                                8.0 * scale
555                            };
556                            let scrollbar_x =
557                                text_area_x + text_area_w as i32 - scrollbar_width as i32;
558
559                            // Block all clicks in scrollbar area
560                            if mx >= scrollbar_x
561                                && mx < text_area_x + text_area_w as i32
562                                && my >= text_area_y
563                                && my < text_area_y + text_area_h as i32
564                            {
565                                clicking_scrollbar = true;
566
567                                // Now check if clicking specifically on the thumb for dragging
568                                let text_area_mx = mx - text_area_x;
569                                let text_area_my = my - text_area_y;
570
571                                let sb_x = text_area_w as i32 - scrollbar_width as i32;
572                                let sb_y_f32 = 4.0 * scale;
573                                let sb_y = sb_y_f32 as i32;
574                                let sb_h_f32 = text_area_h as f32 - 8.0 * scale;
575                                let sb_h = sb_h_f32 as i32;
576
577                                let thumb_h_f32 = (visible_lines as f32 / total_lines as f32
578                                    * sb_h_f32)
579                                    .max(20.0 * scale);
580                                let thumb_h = thumb_h_f32 as i32;
581
582                                let max_scroll = total_lines.saturating_sub(visible_lines);
583                                let thumb_y = if max_scroll > 0 {
584                                    let max_thumb_y = sb_h - thumb_h;
585                                    ((scroll_offset as f32 / max_scroll as f32)
586                                        * max_thumb_y as f32)
587                                        as i32
588                                } else {
589                                    0
590                                };
591
592                                if text_area_mx >= sb_x
593                                    && text_area_mx < sb_x + scrollbar_width as i32
594                                    && text_area_my >= sb_y + thumb_y
595                                    && text_area_my < sb_y + thumb_y + thumb_h
596                                {
597                                    thumb_drag = true;
598                                    thumb_drag_offset = Some(text_area_my - (sb_y + thumb_y));
599                                }
600                            }
601                        }
602                    }
603
604                    // Only process checkbox click if not clicking on scrollbar
605                    if !clicking_scrollbar && checkbox_hovered {
606                        checkbox_checked = !checkbox_checked;
607                        needs_redraw = true;
608                    }
609                }
610                WindowEvent::ButtonRelease(_, _) => {
611                    window_dragging = false;
612                    thumb_drag = false;
613                    thumb_drag_offset = None;
614                }
615                WindowEvent::Scroll(direction) => {
616                    match direction {
617                        crate::backend::ScrollDirection::Up => {
618                            if scroll_offset > 0 {
619                                scroll_offset = scroll_offset.saturating_sub(3);
620                                needs_redraw = true;
621                            }
622                        }
623                        crate::backend::ScrollDirection::Down => {
624                            let max_scroll = total_lines.saturating_sub(visible_lines);
625                            if scroll_offset < max_scroll {
626                                scroll_offset = (scroll_offset + 3).min(max_scroll);
627                                needs_redraw = true;
628                            }
629                        }
630                        _ => {}
631                    }
632                }
633                WindowEvent::TextInput(c) => {
634                    // Handle space for checkbox toggle (TextInput is sent for printable chars)
635                    if *c == ' ' && has_checkbox {
636                        checkbox_checked = !checkbox_checked;
637                        needs_redraw = true;
638                    }
639                }
640                WindowEvent::KeyPress(key_event) => {
641                    let max_scroll = total_lines.saturating_sub(visible_lines);
642
643                    match key_event.keysym {
644                        KEY_UP => {
645                            if scroll_offset > 0 {
646                                scroll_offset = scroll_offset.saturating_sub(1);
647                                needs_redraw = true;
648                            }
649                        }
650                        KEY_DOWN => {
651                            if scroll_offset < max_scroll {
652                                scroll_offset = (scroll_offset + 1).min(max_scroll);
653                                needs_redraw = true;
654                            }
655                        }
656                        KEY_PAGE_UP => {
657                            scroll_offset = scroll_offset.saturating_sub(visible_lines);
658                            needs_redraw = true;
659                        }
660                        KEY_PAGE_DOWN => {
661                            scroll_offset = (scroll_offset + visible_lines).min(max_scroll);
662                            needs_redraw = true;
663                        }
664                        KEY_HOME => {
665                            if scroll_offset > 0 {
666                                scroll_offset = 0;
667                                needs_redraw = true;
668                            }
669                        }
670                        KEY_END => {
671                            if scroll_offset < max_scroll {
672                                scroll_offset = max_scroll;
673                                needs_redraw = true;
674                            }
675                        }
676                        KEY_RETURN => {
677                            return Ok(TextInfoResult::Ok {
678                                checkbox_checked,
679                            });
680                        }
681                        KEY_ESCAPE => {
682                            return Ok(TextInfoResult::Cancelled);
683                        }
684                        _ => {}
685                    }
686                }
687                _ => {}
688            }
689
690            needs_redraw |= ok_button.process_event(&event);
691            needs_redraw |= cancel_button.process_event(&event);
692
693            if ok_button.was_clicked() {
694                return Ok(TextInfoResult::Ok {
695                    checkbox_checked,
696                });
697            }
698            if cancel_button.was_clicked() {
699                return Ok(TextInfoResult::Cancelled);
700            }
701
702            // Batch process pending events
703            while let Some(ev) = window.poll_for_event()? {
704                match &ev {
705                    WindowEvent::CloseRequested => {
706                        return Ok(TextInfoResult::Closed);
707                    }
708                    WindowEvent::CursorEnter(pos) | WindowEvent::CursorMove(pos) => {
709                        last_cursor_pos = Some((pos.x as i32, pos.y as i32));
710                    }
711                    WindowEvent::ButtonPress(button, _modifiers)
712                        if *button == crate::backend::MouseButton::Left =>
713                    {
714                        if let Some((mx, my)) = last_cursor_pos {
715                            if total_lines > visible_lines {
716                                let sb_x = text_area_w as i32 - (10.0 * scale) as i32;
717                                let sb_y_f32 = 4.0 * scale;
718                                let sb_y = sb_y_f32 as i32;
719                                let sb_h_f32 = text_area_h as f32 - 8.0 * scale;
720                                let sb_h = sb_h_f32 as i32;
721
722                                let thumb_h_f32 = (visible_lines as f32 / total_lines as f32
723                                    * sb_h_f32)
724                                    .max(20.0 * scale);
725                                let thumb_h = thumb_h_f32 as i32;
726
727                                let max_scroll = total_lines.saturating_sub(visible_lines);
728                                let max_thumb_y = sb_h - thumb_h;
729                                let thumb_y = if max_scroll > 0 {
730                                    ((scroll_offset as f32 / max_scroll as f32)
731                                        * max_thumb_y as f32)
732                                        as i32
733                                } else {
734                                    0
735                                };
736
737                                if mx >= text_area_x + sb_x
738                                    && mx < text_area_x + sb_x + (6.0 * scale) as i32
739                                    && my >= text_area_y + sb_y + thumb_y
740                                    && my < text_area_y + sb_y + thumb_y + thumb_h
741                                {
742                                    thumb_drag = true;
743                                    thumb_drag_offset = Some(my - (text_area_y + sb_y + thumb_y));
744                                }
745                            }
746                        }
747                    }
748                    WindowEvent::ButtonRelease(_, _) => {
749                        thumb_drag = false;
750                        thumb_drag_offset = None;
751                    }
752                    _ => {}
753                }
754
755                needs_redraw |= ok_button.process_event(&ev);
756                needs_redraw |= cancel_button.process_event(&ev);
757            }
758
759            if needs_redraw {
760                draw(
761                    &mut canvas,
762                    colors,
763                    &font,
764                    &self.title,
765                    &wrapped_lines,
766                    scroll_offset,
767                    visible_lines,
768                    &self.checkbox_text,
769                    checkbox_checked,
770                    checkbox_hovered,
771                    &ok_button,
772                    &cancel_button,
773                    padding,
774                    line_height,
775                    checkbox_size,
776                    text_area_x,
777                    text_area_y,
778                    text_area_w,
779                    text_area_h,
780                    checkbox_y,
781                    scale,
782                    scrollbar_hovered,
783                );
784                window.set_contents(&canvas)?;
785            }
786        }
787    }
788}
789
790impl Default for TextInfoBuilder {
791    fn default() -> Self {
792        Self::new()
793    }
794}
795
796fn darken(color: crate::render::Rgba, amount: f32) -> crate::render::Rgba {
797    rgb(
798        (color.r as f32 * (1.0 - amount)) as u8,
799        (color.g as f32 * (1.0 - amount)) as u8,
800        (color.b as f32 * (1.0 - amount)) as u8,
801    )
802}