Skip to main content

zenity_rs/ui/
list.rs

1//! List selection dialog implementation.
2
3use crate::{
4    backend::{MouseButton, Window, WindowEvent, create_window},
5    error::Error,
6    render::{Canvas, Font, rgb},
7    ui::{
8        BASE_BUTTON_HEIGHT, BASE_BUTTON_SPACING, BASE_CORNER_RADIUS, Colors, KEY_DOWN, KEY_ESCAPE,
9        KEY_LEFT, KEY_LSHIFT, KEY_RETURN, KEY_RIGHT, KEY_RSHIFT, KEY_SPACE, KEY_UP,
10        widgets::{Widget, button::Button},
11    },
12};
13
14const BASE_PADDING: u32 = 16;
15const BASE_ROW_HEIGHT: u32 = 28;
16const BASE_CHECKBOX_SIZE: u32 = 16;
17const BASE_MIN_WIDTH: u32 = 350;
18const BASE_MAX_WIDTH: u32 = 600;
19const BASE_MIN_HEIGHT: u32 = 200;
20const BASE_MAX_HEIGHT: u32 = 450;
21
22/// List dialog result.
23#[derive(Debug, Clone)]
24pub enum ListResult {
25    /// User selected item(s). Contains the values from the first column.
26    Selected(Vec<String>),
27    /// User cancelled.
28    Cancelled,
29    /// Dialog was closed.
30    Closed,
31}
32
33impl ListResult {
34    pub fn exit_code(&self) -> i32 {
35        match self {
36            ListResult::Selected(_) => 0,
37            ListResult::Cancelled => 1,
38            ListResult::Closed => 1,
39        }
40    }
41}
42
43/// List selection mode.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ListMode {
46    /// Single selection (default).
47    Single,
48    /// Multiple selection with checkboxes.
49    Checklist,
50    /// Multiple selection with radio buttons (single select visually).
51    Radiolist,
52    /// Multiple selection without checkboxes.
53    Multiple,
54}
55
56/// List dialog builder.
57pub struct ListBuilder {
58    title: String,
59    text: String,
60    columns: Vec<String>,
61    rows: Vec<Vec<String>>,
62    mode: ListMode,
63    hidden_columns: Vec<usize>,
64    width: Option<u32>,
65    height: Option<u32>,
66    colors: Option<&'static Colors>,
67}
68
69impl ListBuilder {
70    pub fn new() -> Self {
71        Self {
72            title: String::new(),
73            text: String::new(),
74            columns: Vec::new(),
75            rows: Vec::new(),
76            mode: ListMode::Single,
77            hidden_columns: Vec::new(),
78            width: None,
79            height: None,
80            colors: None,
81        }
82    }
83
84    pub fn title(mut self, title: &str) -> Self {
85        self.title = title.to_string();
86        self
87    }
88
89    pub fn text(mut self, text: &str) -> Self {
90        self.text = text.to_string();
91        self
92    }
93
94    /// Add a column header.
95    pub fn column(mut self, name: &str) -> Self {
96        self.columns.push(name.to_string());
97        self
98    }
99
100    /// Add a row of data.
101    pub fn row(mut self, values: Vec<String>) -> Self {
102        self.rows.push(values);
103        self
104    }
105
106    /// Set selection mode.
107    pub fn mode(mut self, mode: ListMode) -> Self {
108        self.mode = mode;
109        self
110    }
111
112    /// Enable checklist mode (multi-select with checkboxes).
113    pub fn checklist(mut self) -> Self {
114        self.mode = ListMode::Checklist;
115        self
116    }
117
118    /// Enable radiolist mode (single-select with radio buttons).
119    pub fn radiolist(mut self) -> Self {
120        self.mode = ListMode::Radiolist;
121        self
122    }
123
124    /// Enable multiple mode (multi-select without checkboxes).
125    pub fn multiple(mut self) -> Self {
126        self.mode = ListMode::Multiple;
127        self
128    }
129
130    pub fn colors(mut self, colors: &'static Colors) -> Self {
131        self.colors = Some(colors);
132        self
133    }
134
135    pub fn width(mut self, width: u32) -> Self {
136        self.width = Some(width);
137        self
138    }
139
140    pub fn height(mut self, height: u32) -> Self {
141        self.height = Some(height);
142        self
143    }
144
145    /// Hide a column by index (1-based, like zenity).
146    /// Hidden columns are not displayed but their values are still included in output.
147    pub fn hide_column(mut self, col: usize) -> Self {
148        if col > 0 {
149            self.hidden_columns.push(col - 1); // Convert to 0-based
150        }
151        self
152    }
153
154    pub fn show(self) -> Result<ListResult, Error> {
155        let colors = self.colors.unwrap_or_else(|| crate::ui::detect_theme());
156
157        // Process rows - for checklist/radiolist, first column is TRUE/FALSE
158        let (rows, mut selected): (Vec<Vec<String>>, Vec<bool>) = match self.mode {
159            ListMode::Checklist | ListMode::Radiolist => {
160                let mut processed_rows = Vec::new();
161                let mut selections = Vec::new();
162
163                for row in &self.rows {
164                    if !row.is_empty() {
165                        let is_selected = row[0].eq_ignore_ascii_case("true");
166                        selections.push(is_selected);
167                        processed_rows.push(row[1..].to_vec());
168                    }
169                }
170                (processed_rows, selections)
171            }
172            ListMode::Single | ListMode::Multiple => {
173                (self.rows.clone(), vec![false; self.rows.len()])
174            }
175        };
176
177        // Columns - skip first column header for checklist/radiolist
178        // (first column is the checkbox, but we keep it for display)
179        let (checkbox_column_header, all_columns): (Option<String>, Vec<&str>) = match self.mode {
180            ListMode::Checklist | ListMode::Radiolist => {
181                let checkbox_header = if !self.columns.is_empty() {
182                    Some(self.columns[0].clone())
183                } else {
184                    None
185                };
186                let data_columns = if !self.columns.is_empty() {
187                    self.columns[1..].iter().map(|s| s.as_str()).collect()
188                } else {
189                    vec![]
190                };
191                (checkbox_header, data_columns)
192            }
193            ListMode::Single | ListMode::Multiple => {
194                (None, self.columns.iter().map(|s| s.as_str()).collect())
195            }
196        };
197
198        // Adjust hidden column indices for radiolist/checklist mode
199        // In these modes, zenity's column 1 is TRUE/FALSE which we strip,
200        // so user's column N becomes internal index N-2 (N-1 for 0-based, then -1 for stripped column)
201        let adjusted_hidden: Vec<usize> = match self.mode {
202            ListMode::Checklist | ListMode::Radiolist => {
203                self.hidden_columns
204                    .iter()
205                    .filter_map(|&col| col.checked_sub(1)) // Subtract 1 more for stripped TRUE/FALSE column
206                    .collect()
207            }
208            ListMode::Single | ListMode::Multiple => self.hidden_columns.clone(),
209        };
210
211        // Determine which columns are visible (not hidden)
212        let visible_col_indices: Vec<usize> = (0..all_columns.len())
213            .filter(|i| !adjusted_hidden.contains(i))
214            .collect();
215
216        // Get visible columns only
217        let columns: Vec<&str> = visible_col_indices
218            .iter()
219            .map(|&i| all_columns[i])
220            .collect();
221
222        // Create display rows with only visible columns (original rows kept for result)
223        let display_rows: Vec<Vec<String>> = rows
224            .iter()
225            .map(|row| {
226                visible_col_indices
227                    .iter()
228                    .filter_map(|&i| row.get(i).cloned())
229                    .collect()
230            })
231            .collect();
232
233        let num_cols = columns.len().max(1);
234        let num_rows = rows.len();
235
236        // Column gap for separation (in logical units at scale 1.0)
237        let logical_column_gap = 16u32;
238
239        // First pass: calculate LOGICAL dimensions using scale 1.0
240        let temp_font = Font::load(1.0);
241
242        // Calculate logical column widths (only for visible columns)
243        let mut logical_col_widths: Vec<u32> = vec![100; num_cols];
244        for (i, col) in columns.iter().enumerate() {
245            let (w, _) = temp_font.render(col).measure();
246            logical_col_widths[i] = logical_col_widths[i].max(w as u32 + 20);
247        }
248        for row in &rows {
249            for (vi, &orig_i) in visible_col_indices.iter().enumerate() {
250                if let Some(cell) = row.get(orig_i) {
251                    let (w, _) = temp_font.render(cell).measure();
252                    logical_col_widths[vi] = logical_col_widths[vi].max(w as u32 + 20);
253                }
254            }
255        }
256        drop(temp_font);
257
258        // Calculate logical total width (including gaps between columns)
259        let logical_checkbox_col = if self.mode != ListMode::Single {
260            BASE_CHECKBOX_SIZE + 16
261        } else {
262            0
263        };
264        let num_gaps = if num_cols > 0 { num_cols - 1 } else { 0 };
265        let logical_content_width: u32 = logical_col_widths.iter().sum::<u32>()
266            + logical_checkbox_col
267            + (num_gaps as u32 * logical_column_gap);
268        let calc_width =
269            (logical_content_width + BASE_PADDING * 2).clamp(BASE_MIN_WIDTH, BASE_MAX_WIDTH);
270
271        // Calculate logical height
272        let logical_title_height = if self.title.is_empty() { 0 } else { 32 };
273        let logical_text_height = if self.text.is_empty() { 0 } else { 24 };
274        let logical_header_height = if columns.is_empty() {
275            0
276        } else {
277            BASE_ROW_HEIGHT
278        };
279        let logical_list_height =
280            (num_rows as u32 * BASE_ROW_HEIGHT).clamp(BASE_ROW_HEIGHT * 3, BASE_MAX_HEIGHT - 100);
281        let calc_height = (BASE_PADDING * 2
282            + logical_title_height
283            + logical_text_height
284            + logical_header_height
285            + logical_list_height
286            + 50)
287            .clamp(BASE_MIN_HEIGHT, BASE_MAX_HEIGHT);
288
289        // Use custom dimensions if provided, otherwise use calculated defaults
290        let logical_width = self.width.unwrap_or(calc_width);
291        let logical_height = self.height.unwrap_or(calc_height);
292
293        // Create window with LOGICAL dimensions
294        let mut window = create_window(logical_width as u16, logical_height as u16)?;
295        window.set_title(if self.title.is_empty() {
296            "Select"
297        } else {
298            &self.title
299        })?;
300
301        // Get the actual scale factor from the window (compositor scale)
302        let scale = window.scale_factor();
303
304        // Now create everything at PHYSICAL scale
305        let font = Font::load(scale);
306
307        // Scale dimensions for physical rendering
308        let padding = (BASE_PADDING as f32 * scale) as u32;
309        let row_height = (BASE_ROW_HEIGHT as f32 * scale) as u32;
310        let checkbox_size = (BASE_CHECKBOX_SIZE as f32 * scale) as u32;
311
312        // Calculate physical dimensions
313        let physical_width = (logical_width as f32 * scale) as u32;
314        let physical_height = (logical_height as f32 * scale) as u32;
315
316        // Recalculate column widths at physical scale
317        let mut col_widths: Vec<u32> = vec![(100.0 * scale) as u32; num_cols];
318        for (i, col) in columns.iter().enumerate() {
319            let (w, _) = font.render(col).measure();
320            col_widths[i] = col_widths[i].max(w as u32 + (20.0 * scale) as u32);
321        }
322        for row in &display_rows {
323            for (i, cell) in row.iter().enumerate() {
324                if i < num_cols {
325                    let (w, _) = font.render(cell).measure();
326                    col_widths[i] = col_widths[i].max(w as u32 + (20.0 * scale) as u32);
327                }
328            }
329        }
330
331        // Calculate physical list dimensions
332        let checkbox_col = if self.mode != ListMode::Single {
333            checkbox_size + (16.0 * scale) as u32
334        } else {
335            0
336        };
337        let text_height = if self.text.is_empty() {
338            0
339        } else {
340            (24.0 * scale) as u32
341        };
342        let list_height = (logical_list_height as f32 * scale) as u32;
343
344        // Calculate total content width including column gaps
345        let column_gap = (16.0 * scale) as u32;
346        let num_gaps = if !col_widths.is_empty() {
347            col_widths.len() - 1
348        } else {
349            0
350        };
351        // Add extra gap after checkbox column for checklist/radiolist modes
352        let checkbox_gap = if self.mode == ListMode::Checklist || self.mode == ListMode::Radiolist {
353            if !col_widths.is_empty() {
354                column_gap
355            } else {
356                0
357            }
358        } else {
359            0
360        };
361        let total_content_width = checkbox_col
362            + checkbox_gap
363            + col_widths.iter().sum::<u32>()
364            + (num_gaps as u32 * column_gap);
365
366        // Create buttons at physical scale
367        let mut ok_button = Button::new("OK", &font, scale);
368        let mut cancel_button = Button::new("Cancel", &font, scale);
369
370        // Layout in physical coordinates
371        let mut y = padding as i32;
372
373        // Calculate title height first
374        let title_height = if self.title.is_empty() {
375            0
376        } else {
377            (24.0 * scale + 8.0 * scale) as u32
378        };
379
380        // Position text below title (if both present)
381        let text_y = if self.text.is_empty() {
382            y
383        } else {
384            y + title_height as i32
385        };
386
387        // Update y position after both title and text
388        if !self.title.is_empty() {
389            y += title_height as i32;
390        }
391        if !self.text.is_empty() {
392            y += text_height as i32 + (8.0 * scale) as i32;
393        }
394
395        let list_x = padding as i32;
396        let list_y = y;
397        let list_w = physical_width - padding * 2;
398        let list_h = list_height;
399        let visible_rows = (list_h / row_height) as usize;
400
401        let button_y =
402            (physical_height - padding - (BASE_BUTTON_HEIGHT as f32 * scale) as u32) as i32;
403        let mut bx = physical_width as i32 - padding as i32;
404        bx -= cancel_button.width() as i32;
405        cancel_button.set_position(bx, button_y);
406        bx -= (BASE_BUTTON_SPACING as f32 * scale) as i32 + ok_button.width() as i32;
407        ok_button.set_position(bx, button_y);
408
409        // Create canvas at PHYSICAL dimensions
410        let mut canvas = Canvas::new(physical_width, physical_height);
411        let mut scroll_offset = 0usize;
412        let mut h_scroll_offset = 0u32;
413        let mut hovered_row: Option<usize> = None;
414        let mut single_selected: Option<usize> = None;
415        let mut h_scroll_mode = false;
416
417        // Track last cursor position for drag scrolling
418        let mut last_cursor_pos: Option<(i32, i32)> = None;
419
420        let mut window_dragging = false;
421
422        // Scrollbar thumb dragging state
423        let mut v_thumb_drag = false;
424        let mut h_thumb_drag = false;
425        let mut v_thumb_drag_offset: Option<i32> = None;
426        let mut h_thumb_drag_offset: Option<i32> = None;
427        let mut v_scrollbar_hovered = false;
428        let mut h_scrollbar_hovered = false;
429
430        // Create sub-canvas for the list area to enable clipping
431        let mut list_canvas = Canvas::new(list_w, list_h);
432
433        // Draw function with scaled parameters
434        let draw = |canvas: &mut Canvas,
435                    list_canvas: &mut Canvas,
436                    colors: &Colors,
437                    font: &Font,
438                    title: &str,
439                    text: &str,
440                    checkbox_column_header: &Option<String>,
441                    columns: &[&str],
442                    rows: &[Vec<String>],
443                    col_widths: &[u32],
444                    selected: &[bool],
445                    single_selected: Option<usize>,
446                    scroll_offset: usize,
447                    h_scroll_offset: u32,
448                    hovered_row: Option<usize>,
449                    mode: ListMode,
450                    ok_button: &Button,
451                    cancel_button: &Button,
452                    total_content_width: u32,
453                    // Scaled parameters
454                    padding: u32,
455                    row_height: u32,
456                    checkbox_size: u32,
457                    checkbox_col: u32,
458                    list_x: i32,
459                    list_y: i32,
460                    list_w: u32,
461                    list_h: u32,
462                    visible_rows: usize,
463                    text_y: i32,
464                    scale: f32,
465                    v_scrollbar_hovered: bool,
466                    h_scrollbar_hovered: bool| {
467            let width = canvas.width() as f32;
468            let height = canvas.height() as f32;
469            let radius = BASE_CORNER_RADIUS * scale;
470
471            canvas.fill_dialog_bg(
472                width,
473                height,
474                colors.window_bg,
475                colors.window_border,
476                colors.window_shadow,
477                radius,
478            );
479
480            // Draw title if present
481            if !title.is_empty() {
482                // Render title with larger font (1.5x normal size)
483                let title_font_size = 18.0 * 1.5 * scale;
484                let title_font = Font::load_with_size(title_font_size);
485                let title_rendered = title_font.render(title).with_color(colors.text).finish();
486                let title_x = (width as i32 - title_rendered.width() as i32) / 2;
487                let title_y = padding as i32;
488                canvas.draw_canvas(&title_rendered, title_x, title_y);
489            }
490
491            // Draw text prompt
492            if !text.is_empty() {
493                let tc = font.render(text).with_color(colors.text).finish();
494                canvas.draw_canvas(&tc, padding as i32, text_y);
495            }
496
497            // Clear list canvas
498            list_canvas.fill(colors.input_bg);
499
500            // List background is already filled above
501
502            // Draw header if columns exist
503            let mut data_y_local = 0i32;
504            if !columns.is_empty() || checkbox_column_header.is_some() {
505                let header_bg = darken(colors.input_bg, 0.05);
506                list_canvas.fill_rect(0.0, 0.0, list_w as f32, row_height as f32, header_bg);
507
508                let mut cx = -(h_scroll_offset as i32);
509
510                // Draw checkbox column header if present
511                if let Some(header) = checkbox_column_header {
512                    let tc = font.render(header).with_color(rgb(140, 140, 140)).finish();
513                    list_canvas.draw_canvas(&tc, cx + (8.0 * scale) as i32, (6.0 * scale) as i32);
514                    cx = checkbox_col as i32 - h_scroll_offset as i32;
515                } else {
516                    cx = checkbox_col as i32 - h_scroll_offset as i32;
517                }
518
519                let column_gap = (16.0 * scale) as i32;
520                // Add gap after checkbox column if there are data columns
521                if !columns.is_empty() && checkbox_column_header.is_some() {
522                    cx += column_gap;
523                }
524                for (i, col) in columns.iter().enumerate() {
525                    let tc = font.render(col).with_color(rgb(140, 140, 140)).finish();
526                    list_canvas.draw_canvas(&tc, cx + (8.0 * scale) as i32, (6.0 * scale) as i32);
527                    cx += col_widths.get(i).copied().unwrap_or((100.0 * scale) as u32) as i32;
528                    // Add gap between columns
529                    if i < columns.len() - 1 {
530                        cx += column_gap;
531                    }
532                }
533
534                // Separator
535                list_canvas.fill_rect(
536                    0.0,
537                    row_height as f32,
538                    list_w as f32,
539                    1.0,
540                    colors.input_border,
541                );
542                data_y_local += row_height as i32 + 1;
543            }
544
545            // Draw rows
546            let data_visible = if columns.is_empty() {
547                visible_rows
548            } else {
549                visible_rows.saturating_sub(1)
550            };
551            for (vi, ri) in
552                (scroll_offset..rows.len().min(scroll_offset + data_visible)).enumerate()
553            {
554                let row = &rows[ri];
555                let ry = data_y_local + (vi as u32 * row_height) as i32;
556
557                // Background
558                let is_hovered = hovered_row == Some(ri);
559                let is_selected = match mode {
560                    ListMode::Single => single_selected == Some(ri),
561                    ListMode::Multiple | ListMode::Checklist | ListMode::Radiolist => {
562                        selected.get(ri).copied().unwrap_or(false)
563                    }
564                };
565
566                let bg = if is_selected {
567                    colors.input_border_focused
568                } else if is_hovered {
569                    darken(colors.input_bg, 0.06)
570                } else if vi % 2 == 1 {
571                    darken(colors.input_bg, 0.02)
572                } else {
573                    colors.input_bg
574                };
575
576                list_canvas.fill_rect(1.0, ry as f32, (list_w - 2) as f32, row_height as f32, bg);
577
578                // Checkbox/Radio
579                if mode == ListMode::Checklist || mode == ListMode::Radiolist {
580                    let check_x = (8.0 * scale) as i32 - h_scroll_offset as i32;
581                    let check_y = ry + ((row_height - checkbox_size) / 2) as i32;
582                    let checked = selected.get(ri).copied().unwrap_or(false);
583
584                    if mode == ListMode::Checklist {
585                        draw_checkbox(
586                            list_canvas,
587                            check_x,
588                            check_y,
589                            checked,
590                            colors,
591                            checkbox_size,
592                            scale,
593                        );
594                    } else {
595                        draw_radio(
596                            list_canvas,
597                            check_x,
598                            check_y,
599                            checked,
600                            colors,
601                            checkbox_size,
602                            scale,
603                        );
604                    }
605                }
606
607                // Cell values
608                let mut cx = checkbox_col as i32 - h_scroll_offset as i32;
609                let column_gap = (16.0 * scale) as i32;
610                // Add gap after checkbox column if there are data columns
611                if !row.is_empty()
612                    && self.mode != ListMode::Single
613                    && self.mode != ListMode::Multiple
614                {
615                    cx += column_gap;
616                }
617                for (ci, cell) in row.iter().enumerate() {
618                    if ci < col_widths.len() {
619                        let text_color = if is_selected {
620                            rgb(255, 255, 255)
621                        } else {
622                            colors.text
623                        };
624                        let tc = font.render(cell).with_color(text_color).finish();
625                        list_canvas.draw_canvas(
626                            &tc,
627                            cx + (8.0 * scale) as i32,
628                            ry + (6.0 * scale) as i32,
629                        );
630                        cx += col_widths[ci] as i32;
631                        // Add gap between columns
632                        if ci < row.len() - 1 {
633                            cx += column_gap;
634                        }
635                    }
636                }
637            }
638
639            // Vertical Scrollbar
640            if rows.len() > data_visible {
641                let sb_x = list_w as i32 - (8.0 * scale) as i32;
642                let sb_h = list_h as f32
643                    - if columns.is_empty() {
644                        0.0
645                    } else {
646                        row_height as f32 + 1.0
647                    };
648                let sb_y = data_y_local as f32;
649                let thumb_h =
650                    ((data_visible as f32 / rows.len() as f32 * sb_h).max(20.0 * scale)).min(sb_h);
651                let max_thumb_y = sb_h - thumb_h;
652                let thumb_y = if rows.len() > data_visible {
653                    scroll_offset as f32 / (rows.len() - data_visible) as f32 * max_thumb_y
654                } else {
655                    0.0
656                };
657
658                let v_scrollbar_width = if v_scrollbar_hovered {
659                    12.0 * scale
660                } else {
661                    8.0 * scale
662                };
663
664                list_canvas.fill_rounded_rect(
665                    sb_x as f32,
666                    sb_y,
667                    v_scrollbar_width - 2.0 * scale,
668                    sb_h,
669                    3.0 * scale,
670                    darken(colors.input_bg, 0.05),
671                );
672                list_canvas.fill_rounded_rect(
673                    sb_x as f32,
674                    sb_y + thumb_y,
675                    v_scrollbar_width - 2.0 * scale,
676                    thumb_h,
677                    3.0 * scale,
678                    if v_scrollbar_hovered {
679                        colors.input_border_focused
680                    } else {
681                        colors.input_border
682                    },
683                );
684            }
685
686            // Horizontal Scrollbar
687            if total_content_width > list_w {
688                let h_scrollbar_width = if h_scrollbar_hovered {
689                    12.0 * scale
690                } else {
691                    8.0 * scale
692                };
693                let sb_x = 0.0;
694                let sb_y = list_h as i32 - h_scrollbar_width as i32;
695                let sb_w = list_w as f32;
696                let max_scroll = total_content_width.saturating_sub(list_w);
697                let thumb_w = ((list_w as f32 / total_content_width as f32 * sb_w)
698                    .max(20.0 * scale))
699                .min(sb_w);
700                let thumb_x = if max_scroll > 0 {
701                    h_scroll_offset as f32 / max_scroll as f32 * (sb_w - thumb_w)
702                } else {
703                    0.0
704                };
705
706                list_canvas.fill_rounded_rect(
707                    sb_x,
708                    sb_y as f32,
709                    sb_w,
710                    h_scrollbar_width - 2.0 * scale,
711                    3.0 * scale,
712                    darken(colors.input_bg, 0.05),
713                );
714                list_canvas.fill_rounded_rect(
715                    sb_x + thumb_x,
716                    sb_y as f32,
717                    thumb_w,
718                    h_scrollbar_width - 2.0 * scale,
719                    3.0 * scale,
720                    if h_scrollbar_hovered {
721                        colors.input_border_focused
722                    } else {
723                        colors.input_border
724                    },
725                );
726            }
727
728            // Border
729            list_canvas.stroke_rounded_rect(
730                0.0,
731                0.0,
732                list_w as f32,
733                list_h as f32,
734                6.0 * scale,
735                colors.input_border,
736                1.0,
737            );
738
739            // Draw the list canvas to main canvas
740            canvas.draw_canvas(list_canvas, list_x, list_y);
741
742            // Buttons
743            ok_button.draw_to(canvas, colors, font);
744            cancel_button.draw_to(canvas, colors, font);
745        };
746
747        // Initial draw
748        draw(
749            &mut canvas,
750            &mut list_canvas,
751            colors,
752            &font,
753            &self.title,
754            &self.text,
755            &checkbox_column_header,
756            &columns,
757            &display_rows,
758            &col_widths,
759            &selected,
760            single_selected,
761            scroll_offset,
762            h_scroll_offset,
763            hovered_row,
764            self.mode,
765            &ok_button,
766            &cancel_button,
767            total_content_width,
768            padding,
769            row_height,
770            checkbox_size,
771            checkbox_col,
772            list_x,
773            list_y,
774            list_w,
775            list_h,
776            visible_rows,
777            text_y,
778            scale,
779            v_scrollbar_hovered,
780            h_scrollbar_hovered,
781        );
782        window.set_contents(&canvas)?;
783        window.show()?;
784
785        let header_height_px = if columns.is_empty() {
786            0
787        } else {
788            row_height + 1
789        };
790        let data_y = list_y + header_height_px as i32;
791        let data_visible = if columns.is_empty() {
792            visible_rows
793        } else {
794            visible_rows.saturating_sub(1)
795        };
796        loop {
797            let event = window.wait_for_event()?;
798            let mut needs_redraw = false;
799
800            match &event {
801                WindowEvent::CloseRequested => return Ok(ListResult::Closed),
802                WindowEvent::RedrawRequested => needs_redraw = true,
803                WindowEvent::CursorMove(pos) => {
804                    if window_dragging {
805                        let _ = window.start_drag();
806                        window_dragging = false;
807                    }
808
809                    let mx = pos.x as i32;
810                    let my = pos.y as i32;
811
812                    // Store current cursor position
813                    last_cursor_pos = Some((mx, my));
814
815                    // Handle scrollbar thumb dragging
816                    if v_thumb_drag || h_thumb_drag {
817                        let list_mx = mx - list_x;
818                        let list_my = my - list_y;
819
820                        if v_thumb_drag && rows.len() > data_visible {
821                            let sb_h_f32 = list_h as f32
822                                - if columns.is_empty() {
823                                    0.0
824                                } else {
825                                    row_height as f32 + 1.0
826                                };
827                            let sb_h = sb_h_f32 as i32;
828                            let sb_y = if columns.is_empty() {
829                                0
830                            } else {
831                                (row_height + 1) as i32
832                            };
833                            let thumb_h_f32 = ((data_visible as f32 / rows.len() as f32
834                                * sb_h_f32)
835                                .max(20.0 * scale))
836                            .min(sb_h_f32);
837                            let thumb_h = thumb_h_f32 as i32;
838                            let max_thumb_y = sb_h - thumb_h;
839
840                            // Calculate new scroll offset from thumb position
841                            // Use the drag offset to maintain the relative position from where user clicked
842                            let offset = v_thumb_drag_offset.unwrap_or(thumb_h / 2);
843                            // list_my is relative to list canvas, need to adjust for scrollbar position
844                            let thumb_y = (list_my - sb_y - offset).clamp(0, max_thumb_y);
845                            let scroll_ratio = if max_thumb_y > 0 {
846                                thumb_y as f32 / max_thumb_y as f32
847                            } else {
848                                0.0
849                            };
850                            scroll_offset = ((scroll_ratio * (rows.len() - data_visible) as f32)
851                                as usize)
852                                .clamp(0, rows.len().saturating_sub(data_visible));
853                            needs_redraw = true;
854                        }
855
856                        if h_thumb_drag && total_content_width > list_w {
857                            let sb_w_f32 = list_w as f32;
858                            let sb_w = list_w as i32;
859                            let max_scroll_u32 = total_content_width.saturating_sub(list_w);
860                            let max_scroll = (max_scroll_u32 as i32).max(1);
861                            let thumb_w_f32 = ((list_w as f32 / total_content_width as f32
862                                * sb_w_f32)
863                                .max(20.0 * scale))
864                            .min(sb_w_f32);
865                            let thumb_w = thumb_w_f32 as i32;
866                            let max_thumb_x = sb_w - thumb_w;
867
868                            // Calculate new horizontal scroll offset from thumb position
869                            // Use the drag offset to maintain the relative position from where user clicked
870                            let offset = h_thumb_drag_offset.unwrap_or(thumb_w / 2);
871                            let thumb_x = (list_mx - offset).clamp(0, max_thumb_x);
872                            let scroll_ratio = if max_scroll > 0 {
873                                thumb_x as f32 / max_thumb_x as f32
874                            } else {
875                                0.0
876                            };
877                            h_scroll_offset = ((scroll_ratio * max_scroll as f32) as u32)
878                                .clamp(0, max_scroll_u32);
879                            needs_redraw = true;
880                        }
881                    } else {
882                        let old_hovered = hovered_row;
883                        hovered_row = None;
884
885                        // Update scrollbar hover states
886                        let v_scrollbar_width = if v_scrollbar_hovered {
887                            12.0 * scale
888                        } else {
889                            8.0 * scale
890                        };
891                        let v_scrollbar_x = list_w as i32 - v_scrollbar_width as i32;
892                        let h_scrollbar_width = if h_scrollbar_hovered {
893                            12.0 * scale
894                        } else {
895                            8.0 * scale
896                        };
897
898                        v_scrollbar_hovered = rows.len() > data_visible
899                            && mx >= list_x + v_scrollbar_x
900                            && mx < list_x + list_w as i32
901                            && my >= list_y
902                            && my < list_y + list_h as i32;
903
904                        h_scrollbar_hovered = total_content_width > list_w
905                            && mx >= list_x
906                            && mx < list_x + list_w as i32
907                            && my >= list_y + list_h as i32 - h_scrollbar_width as i32
908                            && my < list_y + list_h as i32;
909
910                        // Check row hover (only if not over scrollbar)
911                        let effective_v_scrollbar_width =
912                            if v_scrollbar_hovered && rows.len() > data_visible {
913                                12.0 * scale
914                            } else if rows.len() > data_visible {
915                                8.0 * scale
916                            } else {
917                                0.0
918                            };
919
920                        if mx >= list_x
921                            && mx < list_x + list_w as i32 - effective_v_scrollbar_width as i32
922                            && my >= data_y
923                            && my < list_y + list_h as i32
924                        {
925                            let rel_y = (my - data_y) as usize;
926                            let ri = scroll_offset + rel_y / row_height as usize;
927                            if ri < rows.len() {
928                                hovered_row = Some(ri);
929                            }
930                        }
931
932                        if old_hovered != hovered_row {
933                            needs_redraw = true;
934                        }
935                    }
936                }
937                WindowEvent::ButtonPress(MouseButton::Left, mods) => {
938                    window_dragging = true;
939                    let mut clicking_scrollbar = false;
940
941                    // Check if clicking anywhere in scrollbar area (thumb OR track)
942                    if let Some((mx, my)) = last_cursor_pos {
943                        // Check if click is in list area (convert to list canvas coords)
944                        let list_mx = mx - list_x;
945                        let list_my = my - list_y;
946
947                        if list_mx >= 0
948                            && list_mx < list_w as i32
949                            && list_my >= 0
950                            && list_my < list_h as i32
951                        {
952                            // Vertical scrollbar area
953                            if rows.len() > data_visible {
954                                let v_scrollbar_width = if v_scrollbar_hovered {
955                                    12.0 * scale
956                                } else {
957                                    8.0 * scale
958                                };
959                                let sb_x = list_w as i32 - v_scrollbar_width as i32;
960
961                                // Block all clicks in vertical scrollbar area
962                                if list_mx >= sb_x {
963                                    clicking_scrollbar = true;
964
965                                    let sb_h_f32 = list_h as f32
966                                        - if columns.is_empty() {
967                                            0.0
968                                        } else {
969                                            row_height as f32 + 1.0
970                                        };
971                                    let sb_y = if columns.is_empty() {
972                                        0
973                                    } else {
974                                        (row_height + 1) as i32
975                                    };
976                                    let thumb_h_f32 = ((data_visible as f32 / rows.len() as f32
977                                        * sb_h_f32)
978                                        .max(20.0 * scale))
979                                    .min(sb_h_f32);
980                                    let thumb_h = thumb_h_f32 as i32;
981                                    let max_thumb_y = (sb_h_f32 - thumb_h_f32) as i32;
982                                    let thumb_y = if rows.len() > data_visible {
983                                        (scroll_offset as f32 / (rows.len() - data_visible) as f32
984                                            * max_thumb_y as f32)
985                                            as i32
986                                    } else {
987                                        0
988                                    };
989
990                                    // Check if clicking specifically on the thumb for dragging
991                                    if list_my >= sb_y + thumb_y
992                                        && list_my < sb_y + thumb_y + thumb_h
993                                    {
994                                        v_thumb_drag = true;
995                                        v_thumb_drag_offset = Some(list_my - (sb_y + thumb_y));
996                                    }
997                                }
998                            }
999
1000                            // Horizontal scrollbar area
1001                            if total_content_width > list_w {
1002                                let h_scrollbar_width = if h_scrollbar_hovered {
1003                                    12.0 * scale
1004                                } else {
1005                                    8.0 * scale
1006                                };
1007                                let sb_h = h_scrollbar_width as i32;
1008                                let sb_y = list_h as i32 - sb_h;
1009
1010                                // Block all clicks in horizontal scrollbar area
1011                                if list_my >= sb_y {
1012                                    clicking_scrollbar = true;
1013
1014                                    let sb_w_f32 = list_w as f32;
1015                                    let sb_w = list_w as i32;
1016                                    let max_scroll_u32 = total_content_width.saturating_sub(list_w);
1017                                    let max_scroll = (max_scroll_u32 as i32).max(1);
1018                                    let thumb_w_f32 =
1019                                        ((list_w as f32 / total_content_width as f32 * sb_w_f32)
1020                                            .max(20.0 * scale))
1021                                        .min(sb_w_f32);
1022                                    let thumb_w = thumb_w_f32 as i32;
1023                                    let max_thumb_x = sb_w - thumb_w;
1024                                    let thumb_x = if max_scroll > 0 {
1025                                        (h_scroll_offset as f32 / max_scroll as f32
1026                                            * max_thumb_x as f32)
1027                                            as i32
1028                                    } else {
1029                                        0
1030                                    };
1031
1032                                    // Check if clicking specifically on the thumb for dragging
1033                                    if list_mx >= thumb_x && list_mx < thumb_x + thumb_w {
1034                                        h_thumb_drag = true;
1035                                        h_thumb_drag_offset = Some(list_mx - thumb_x);
1036                                    }
1037                                }
1038                            }
1039                        }
1040                    }
1041
1042                    // Only process row selection if not clicking on scrollbar
1043                    if !clicking_scrollbar {
1044                        if let Some(ri) = hovered_row {
1045                            match self.mode {
1046                                ListMode::Single => {
1047                                    single_selected = Some(ri);
1048                                }
1049                                ListMode::Multiple => {
1050                                    // Only toggle selection if Ctrl is held, otherwise select only this item
1051                                    if mods.contains(crate::backend::Modifiers::CTRL) {
1052                                        if let Some(sel) = selected.get_mut(ri) {
1053                                            *sel = !*sel;
1054                                        }
1055                                    } else {
1056                                        for s in selected.iter_mut() {
1057                                            *s = false;
1058                                        }
1059                                        if let Some(sel) = selected.get_mut(ri) {
1060                                            *sel = true;
1061                                        }
1062                                    }
1063                                }
1064                                ListMode::Checklist => {
1065                                    if let Some(sel) = selected.get_mut(ri) {
1066                                        *sel = !*sel;
1067                                    }
1068                                }
1069                                ListMode::Radiolist => {
1070                                    // Only one can be selected
1071                                    for s in selected.iter_mut() {
1072                                        *s = false;
1073                                    }
1074                                    if let Some(sel) = selected.get_mut(ri) {
1075                                        *sel = true;
1076                                    }
1077                                }
1078                            }
1079                            needs_redraw = true;
1080                        }
1081                    }
1082                }
1083                WindowEvent::ButtonRelease(_, _) => {
1084                    window_dragging = false;
1085                    // End scrollbar thumb dragging
1086                    v_thumb_drag = false;
1087                    h_thumb_drag = false;
1088                    v_thumb_drag_offset = None;
1089                    h_thumb_drag_offset = None;
1090                }
1091                WindowEvent::Scroll(direction) => {
1092                    if h_scroll_mode {
1093                        // Shift + wheel: horizontal scroll
1094                        match direction {
1095                            crate::backend::ScrollDirection::Up => {
1096                                if total_content_width > list_w {
1097                                    h_scroll_offset = h_scroll_offset.saturating_sub(100);
1098                                    needs_redraw = true;
1099                                }
1100                            }
1101                            crate::backend::ScrollDirection::Down => {
1102                                if total_content_width > list_w {
1103                                    let max_scroll = total_content_width.saturating_sub(list_w);
1104                                    h_scroll_offset = (h_scroll_offset + 100).min(max_scroll);
1105                                    needs_redraw = true;
1106                                }
1107                            }
1108                            _ => {}
1109                        }
1110                    } else {
1111                        // Normal wheel: vertical scroll
1112                        match direction {
1113                            crate::backend::ScrollDirection::Up => {
1114                                if scroll_offset > 0 {
1115                                    scroll_offset = scroll_offset.saturating_sub(2);
1116                                    needs_redraw = true;
1117                                }
1118                            }
1119                            crate::backend::ScrollDirection::Down => {
1120                                if scroll_offset + data_visible < rows.len() {
1121                                    scroll_offset = (scroll_offset + 2)
1122                                        .min(rows.len().saturating_sub(data_visible));
1123                                    needs_redraw = true;
1124                                }
1125                            }
1126                            crate::backend::ScrollDirection::Left => {
1127                                if total_content_width > list_w {
1128                                    h_scroll_offset = h_scroll_offset.saturating_sub(100);
1129                                    needs_redraw = true;
1130                                }
1131                            }
1132                            crate::backend::ScrollDirection::Right => {
1133                                if total_content_width > list_w {
1134                                    let max_scroll = total_content_width.saturating_sub(list_w);
1135                                    h_scroll_offset = (h_scroll_offset + 100).min(max_scroll);
1136                                    needs_redraw = true;
1137                                }
1138                            }
1139                        }
1140                    }
1141                }
1142                WindowEvent::KeyPress(key_event) => {
1143                    // Handle shift for scroll mode
1144                    if key_event.keysym == KEY_LSHIFT || key_event.keysym == KEY_RSHIFT {
1145                        h_scroll_mode = true;
1146                        continue;
1147                    } else {
1148                        h_scroll_mode = false;
1149                    }
1150
1151                    match key_event.keysym {
1152                        KEY_UP => {
1153                            if self.mode == ListMode::Single {
1154                                if let Some(sel) = single_selected {
1155                                    if sel > 0 {
1156                                        single_selected = Some(sel - 1);
1157                                        if sel - 1 < scroll_offset {
1158                                            scroll_offset = sel - 1;
1159                                        }
1160                                        needs_redraw = true;
1161                                    }
1162                                } else if !rows.is_empty() {
1163                                    single_selected = Some(0);
1164                                    needs_redraw = true;
1165                                }
1166                            } else if self.mode == ListMode::Multiple {
1167                                let last_selected = selected.iter().position(|&s| s);
1168                                if let Some(last) = last_selected {
1169                                    if last > 0 {
1170                                        single_selected = Some(last - 1);
1171                                        if last - 1 < scroll_offset {
1172                                            scroll_offset = last - 1;
1173                                        }
1174                                        needs_redraw = true;
1175                                    }
1176                                } else if !rows.is_empty() {
1177                                    single_selected = Some(0);
1178                                    needs_redraw = true;
1179                                }
1180                            }
1181                        }
1182                        KEY_DOWN => {
1183                            if self.mode == ListMode::Single {
1184                                if let Some(sel) = single_selected {
1185                                    if sel + 1 < rows.len() {
1186                                        single_selected = Some(sel + 1);
1187                                        if sel + 1 >= scroll_offset + data_visible {
1188                                            scroll_offset = sel + 2 - data_visible;
1189                                        }
1190                                        needs_redraw = true;
1191                                    }
1192                                } else if !rows.is_empty() {
1193                                    single_selected = Some(0);
1194                                    needs_redraw = true;
1195                                }
1196                            } else if self.mode == ListMode::Multiple {
1197                                let last_selected = selected.iter().position(|&s| s);
1198                                if let Some(last) = last_selected {
1199                                    if last + 1 < rows.len() {
1200                                        single_selected = Some(last + 1);
1201                                        if last + 1 >= scroll_offset + data_visible {
1202                                            scroll_offset = last + 2 - data_visible;
1203                                        }
1204                                        needs_redraw = true;
1205                                    }
1206                                } else if !rows.is_empty() {
1207                                    single_selected = Some(0);
1208                                    needs_redraw = true;
1209                                }
1210                            }
1211                        }
1212                        KEY_LEFT => {
1213                            if total_content_width > list_w {
1214                                h_scroll_offset = h_scroll_offset.saturating_sub(100);
1215                                needs_redraw = true;
1216                            }
1217                        }
1218                        KEY_RIGHT => {
1219                            if total_content_width > list_w {
1220                                let max_scroll = total_content_width.saturating_sub(list_w);
1221                                h_scroll_offset = (h_scroll_offset + 100).min(max_scroll);
1222                                needs_redraw = true;
1223                            }
1224                        }
1225                        KEY_SPACE => {
1226                            if self.mode == ListMode::Checklist || self.mode == ListMode::Multiple {
1227                                if let Some(ri) = hovered_row.or(single_selected) {
1228                                    if let Some(sel) = selected.get_mut(ri) {
1229                                        *sel = !*sel;
1230                                        needs_redraw = true;
1231                                    }
1232                                }
1233                            }
1234                        }
1235                        KEY_RETURN => {
1236                            // Return selected
1237                            return Ok(get_result(&rows, &selected, single_selected, self.mode));
1238                        }
1239                        KEY_ESCAPE => {
1240                            return Ok(ListResult::Cancelled);
1241                        }
1242                        _ => {}
1243                    }
1244                }
1245                WindowEvent::KeyRelease(key_event) => {
1246                    // Handle shift release for scroll mode
1247                    if key_event.keysym == KEY_LSHIFT || key_event.keysym == KEY_RSHIFT {
1248                        h_scroll_mode = false;
1249                    }
1250                }
1251                _ => {}
1252            }
1253
1254            needs_redraw |= ok_button.process_event(&event);
1255            needs_redraw |= cancel_button.process_event(&event);
1256
1257            if ok_button.was_clicked() {
1258                return Ok(get_result(&rows, &selected, single_selected, self.mode));
1259            }
1260            if cancel_button.was_clicked() {
1261                return Ok(ListResult::Cancelled);
1262            }
1263
1264            while let Some(ev) = window.poll_for_event()? {
1265                match &ev {
1266                    WindowEvent::CloseRequested => {
1267                        return Ok(ListResult::Closed);
1268                    }
1269                    WindowEvent::CursorEnter(pos) | WindowEvent::CursorMove(pos) => {
1270                        last_cursor_pos = Some((pos.x as i32, pos.y as i32));
1271                    }
1272                    WindowEvent::ButtonPress(button, _modifiers)
1273                        if *button == MouseButton::Left =>
1274                    {
1275                        if let Some((list_mx, list_my)) = last_cursor_pos {
1276                            // Check vertical scrollbar thumb
1277                            if rows.len() > data_visible {
1278                                let sb_x = list_w as i32 - (8.0 * scale) as i32;
1279                                let sb_h_f32 = list_h as f32
1280                                    - if columns.is_empty() {
1281                                        0.0
1282                                    } else {
1283                                        row_height as f32 + 1.0
1284                                    };
1285                                let thumb_h_f32 = ((data_visible as f32 / rows.len() as f32
1286                                    * sb_h_f32)
1287                                    .max(20.0 * scale))
1288                                .min(sb_h_f32);
1289                                let thumb_h = thumb_h_f32 as i32;
1290                                let max_thumb_y = (sb_h_f32 - thumb_h_f32) as i32;
1291                                let thumb_y = if rows.len() > data_visible {
1292                                    (scroll_offset as f32 / (rows.len() - data_visible) as f32
1293                                        * max_thumb_y as f32)
1294                                        as i32
1295                                } else {
1296                                    0
1297                                };
1298
1299                                if list_mx >= sb_x
1300                                    && list_mx < sb_x + (8.0 * scale) as i32
1301                                    && list_my >= thumb_y
1302                                    && list_my < thumb_y + thumb_h
1303                                {
1304                                    v_thumb_drag = true;
1305                                    v_thumb_drag_offset = Some(list_my - thumb_y);
1306                                }
1307                            }
1308
1309                            // Check horizontal scrollbar thumb
1310                            if total_content_width > list_w {
1311                                let sb_h = (6.0 * scale) as i32;
1312                                let sb_y = list_h as i32 - sb_h;
1313                                let sb_w_f32 = list_w as f32;
1314                                let sb_w = list_w as i32;
1315                                let max_scroll_u32 = total_content_width.saturating_sub(list_w);
1316                                let max_scroll = (max_scroll_u32 as i32).max(1);
1317                                let thumb_w_f32 = ((list_w as f32 / total_content_width as f32
1318                                    * sb_w_f32)
1319                                    .max(20.0 * scale))
1320                                .min(sb_w_f32);
1321                                let thumb_w = thumb_w_f32 as i32;
1322                                let max_thumb_x = sb_w - thumb_w;
1323                                let thumb_x = if max_scroll > 0 {
1324                                    (h_scroll_offset as f32 / max_scroll as f32
1325                                        * max_thumb_x as f32)
1326                                        as i32
1327                                } else {
1328                                    0
1329                                };
1330
1331                                if list_my >= sb_y
1332                                    && list_my < sb_y + sb_h
1333                                    && list_mx >= thumb_x
1334                                    && list_mx < thumb_x + thumb_w
1335                                {
1336                                    h_thumb_drag = true;
1337                                    h_thumb_drag_offset = Some(list_mx - thumb_x);
1338                                }
1339                            }
1340                        }
1341                    }
1342                    WindowEvent::ButtonRelease(_, _) => {
1343                        v_thumb_drag = false;
1344                        h_thumb_drag = false;
1345                        v_thumb_drag_offset = None;
1346                        h_thumb_drag_offset = None;
1347                    }
1348                    _ => {}
1349                }
1350
1351                needs_redraw |= ok_button.process_event(&ev);
1352                needs_redraw |= cancel_button.process_event(&ev);
1353            }
1354
1355            if needs_redraw {
1356                draw(
1357                    &mut canvas,
1358                    &mut list_canvas,
1359                    colors,
1360                    &font,
1361                    &self.title,
1362                    &self.text,
1363                    &checkbox_column_header,
1364                    &columns,
1365                    &display_rows,
1366                    &col_widths,
1367                    &selected,
1368                    single_selected,
1369                    scroll_offset,
1370                    h_scroll_offset,
1371                    hovered_row,
1372                    self.mode,
1373                    &ok_button,
1374                    &cancel_button,
1375                    total_content_width,
1376                    padding,
1377                    row_height,
1378                    checkbox_size,
1379                    checkbox_col,
1380                    list_x,
1381                    list_y,
1382                    list_w,
1383                    list_h,
1384                    visible_rows,
1385                    text_y,
1386                    scale,
1387                    v_scrollbar_hovered,
1388                    h_scrollbar_hovered,
1389                );
1390                window.set_contents(&canvas)?;
1391            }
1392        }
1393    }
1394}
1395
1396impl Default for ListBuilder {
1397    fn default() -> Self {
1398        Self::new()
1399    }
1400}
1401
1402fn get_result(
1403    rows: &[Vec<String>],
1404    selected: &[bool],
1405    single_selected: Option<usize>,
1406    mode: ListMode,
1407) -> ListResult {
1408    let mut result = Vec::new();
1409
1410    match mode {
1411        ListMode::Single => {
1412            if let Some(idx) = single_selected {
1413                if let Some(row) = rows.get(idx) {
1414                    if let Some(val) = row.first() {
1415                        result.push(val.clone());
1416                    }
1417                }
1418            }
1419        }
1420        ListMode::Multiple | ListMode::Checklist | ListMode::Radiolist => {
1421            for (i, &sel) in selected.iter().enumerate() {
1422                if sel {
1423                    if let Some(row) = rows.get(i) {
1424                        if let Some(val) = row.first() {
1425                            result.push(val.clone());
1426                        }
1427                    }
1428                }
1429            }
1430        }
1431    }
1432
1433    if result.is_empty() {
1434        ListResult::Cancelled
1435    } else {
1436        ListResult::Selected(result)
1437    }
1438}
1439
1440fn darken(color: crate::render::Rgba, amount: f32) -> crate::render::Rgba {
1441    rgb(
1442        (color.r as f32 * (1.0 - amount)) as u8,
1443        (color.g as f32 * (1.0 - amount)) as u8,
1444        (color.b as f32 * (1.0 - amount)) as u8,
1445    )
1446}
1447
1448fn draw_checkbox(
1449    canvas: &mut Canvas,
1450    x: i32,
1451    y: i32,
1452    checked: bool,
1453    colors: &Colors,
1454    checkbox_size: u32,
1455    scale: f32,
1456) {
1457    // Box
1458    canvas.fill_rounded_rect(
1459        x as f32,
1460        y as f32,
1461        checkbox_size as f32,
1462        checkbox_size as f32,
1463        3.0 * scale,
1464        colors.input_bg,
1465    );
1466    canvas.stroke_rounded_rect(
1467        x as f32,
1468        y as f32,
1469        checkbox_size as f32,
1470        checkbox_size as f32,
1471        3.0 * scale,
1472        colors.input_border,
1473        1.0,
1474    );
1475
1476    // Check mark
1477    if checked {
1478        let inset = (3.0 * scale) as i32;
1479        canvas.fill_rounded_rect(
1480            (x + inset) as f32,
1481            (y + inset) as f32,
1482            (checkbox_size as i32 - inset * 2) as f32,
1483            (checkbox_size as i32 - inset * 2) as f32,
1484            2.0 * scale,
1485            colors.input_border_focused,
1486        );
1487    }
1488}
1489
1490fn draw_radio(
1491    canvas: &mut Canvas,
1492    x: i32,
1493    y: i32,
1494    checked: bool,
1495    colors: &Colors,
1496    checkbox_size: u32,
1497    _scale: f32,
1498) {
1499    let cx = x as f32 + checkbox_size as f32 / 2.0;
1500    let cy = y as f32 + checkbox_size as f32 / 2.0;
1501    let r = checkbox_size as f32 / 2.0;
1502
1503    // Outer circle (using rounded rect as approximation)
1504    canvas.fill_rounded_rect(
1505        x as f32,
1506        y as f32,
1507        checkbox_size as f32,
1508        checkbox_size as f32,
1509        r,
1510        colors.input_bg,
1511    );
1512    canvas.stroke_rounded_rect(
1513        x as f32,
1514        y as f32,
1515        checkbox_size as f32,
1516        checkbox_size as f32,
1517        r,
1518        colors.input_border,
1519        1.0,
1520    );
1521
1522    // Inner dot
1523    if checked {
1524        let inner_r = r * 0.5;
1525        canvas.fill_rounded_rect(
1526            cx - inner_r,
1527            cy - inner_r,
1528            inner_r * 2.0,
1529            inner_r * 2.0,
1530            inner_r,
1531            colors.input_border_focused,
1532        );
1533    }
1534}