1use 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#[derive(Debug, Clone)]
24pub enum ListResult {
25 Selected(Vec<String>),
27 Cancelled,
29 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ListMode {
46 Single,
48 Checklist,
50 Radiolist,
52 Multiple,
54}
55
56pub 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 pub fn column(mut self, name: &str) -> Self {
96 self.columns.push(name.to_string());
97 self
98 }
99
100 pub fn row(mut self, values: Vec<String>) -> Self {
102 self.rows.push(values);
103 self
104 }
105
106 pub fn mode(mut self, mode: ListMode) -> Self {
108 self.mode = mode;
109 self
110 }
111
112 pub fn checklist(mut self) -> Self {
114 self.mode = ListMode::Checklist;
115 self
116 }
117
118 pub fn radiolist(mut self) -> Self {
120 self.mode = ListMode::Radiolist;
121 self
122 }
123
124 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 pub fn hide_column(mut self, col: usize) -> Self {
148 if col > 0 {
149 self.hidden_columns.push(col - 1); }
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 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 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 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)) .collect()
207 }
208 ListMode::Single | ListMode::Multiple => self.hidden_columns.clone(),
209 };
210
211 let visible_col_indices: Vec<usize> = (0..all_columns.len())
213 .filter(|i| !adjusted_hidden.contains(i))
214 .collect();
215
216 let columns: Vec<&str> = visible_col_indices
218 .iter()
219 .map(|&i| all_columns[i])
220 .collect();
221
222 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 let logical_column_gap = 16u32;
238
239 let temp_font = Font::load(1.0);
241
242 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 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 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 let logical_width = self.width.unwrap_or(calc_width);
291 let logical_height = self.height.unwrap_or(calc_height);
292
293 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 let scale = window.scale_factor();
303
304 let font = Font::load(scale);
306
307 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 let physical_width = (logical_width as f32 * scale) as u32;
314 let physical_height = (logical_height as f32 * scale) as u32;
315
316 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 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 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 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 let mut ok_button = Button::new("OK", &font, scale);
368 let mut cancel_button = Button::new("Cancel", &font, scale);
369
370 let mut y = padding as i32;
372
373 let title_height = if self.title.is_empty() {
375 0
376 } else {
377 (24.0 * scale + 8.0 * scale) as u32
378 };
379
380 let text_y = if self.text.is_empty() {
382 y
383 } else {
384 y + title_height as i32
385 };
386
387 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 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 let mut last_cursor_pos: Option<(i32, i32)> = None;
419
420 let mut window_dragging = false;
421
422 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 let mut list_canvas = Canvas::new(list_w, list_h);
432
433 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 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 if !title.is_empty() {
482 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 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 list_canvas.fill(colors.input_bg);
499
500 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 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 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 if i < columns.len() - 1 {
530 cx += column_gap;
531 }
532 }
533
534 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 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 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 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 let mut cx = checkbox_col as i32 - h_scroll_offset as i32;
609 let column_gap = (16.0 * scale) as i32;
610 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 if ci < row.len() - 1 {
633 cx += column_gap;
634 }
635 }
636 }
637 }
638
639 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 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 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 canvas.draw_canvas(list_canvas, list_x, list_y);
741
742 ok_button.draw_to(canvas, colors, font);
744 cancel_button.draw_to(canvas, colors, font);
745 };
746
747 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 last_cursor_pos = Some((mx, my));
814
815 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 let offset = v_thumb_drag_offset.unwrap_or(thumb_h / 2);
843 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 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 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 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 if let Some((mx, my)) = last_cursor_pos {
943 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}