1use super::*;
2
3impl Context {
4 pub fn grid(&mut self, cols: u32, f: impl FnOnce(&mut Context)) -> Response {
21 slt_assert(cols > 0, "grid() requires at least 1 column");
22 let interaction_id = self.next_interaction_id();
23 let border = self.theme.border;
24
25 self.commands
26 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
27 direction: Direction::Column,
28 gap: 0,
29 align: Align::Start,
30 align_self: None,
31 justify: Justify::Start,
32 border: None,
33 border_sides: BorderSides::all(),
34 border_style: Style::new().fg(border),
35 bg_color: None,
36 padding: Padding::default(),
37 margin: Margin::default(),
38 constraints: Constraints::default(),
39 title: None,
40 grow: 0,
41 group_name: None,
42 })));
43
44 let children_start = self.commands.len();
45 f(self);
46 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
47
48 let elements = collect_grid_elements(child_commands);
49
50 let cols = cols.max(1) as usize;
51 for row in elements.chunks(cols) {
52 self.skip_interaction_slot();
53 self.commands
54 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
55 direction: Direction::Row,
56 gap: 0,
57 align: Align::Start,
58 align_self: None,
59 justify: Justify::Start,
60 border: None,
61 border_sides: BorderSides::all(),
62 border_style: Style::new().fg(border),
63 bg_color: None,
64 padding: Padding::default(),
65 margin: Margin::default(),
66 constraints: Constraints::default(),
67 title: None,
68 grow: 0,
69 group_name: None,
70 })));
71
72 for element in row {
73 self.skip_interaction_slot();
74 self.commands
75 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
76 direction: Direction::Column,
77 gap: 0,
78 align: Align::Start,
79 align_self: None,
80 justify: Justify::Start,
81 border: None,
82 border_sides: BorderSides::all(),
83 border_style: Style::new().fg(border),
84 bg_color: None,
85 padding: Padding::default(),
86 margin: Margin::default(),
87 constraints: Constraints::default(),
88 title: None,
89 grow: 1,
90 group_name: None,
91 })));
92 self.commands.extend(element.iter().cloned());
93 self.commands.push(Command::EndContainer);
94 }
95
96 self.commands.push(Command::EndContainer);
97 }
98
99 self.commands.push(Command::EndContainer);
100 self.rollback.last_text_idx = None;
101
102 self.response_for(interaction_id)
103 }
104
105 pub fn grid_with(&mut self, columns: &[GridColumn], f: impl FnOnce(&mut Context)) -> Response {
136 let cols = columns.len().max(1);
137 let interaction_id = self.next_interaction_id();
138 let border = self.theme.border;
139
140 self.commands
141 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
142 direction: Direction::Column,
143 gap: 0,
144 align: Align::Start,
145 align_self: None,
146 justify: Justify::Start,
147 border: None,
148 border_sides: BorderSides::all(),
149 border_style: Style::new().fg(border),
150 bg_color: None,
151 padding: Padding::default(),
152 margin: Margin::default(),
153 constraints: Constraints::default(),
154 title: None,
155 grow: 0,
156 group_name: None,
157 })));
158
159 let children_start = self.commands.len();
160 f(self);
161 let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
162
163 let elements = collect_grid_elements(child_commands);
164
165 for row in elements.chunks(cols) {
166 self.skip_interaction_slot();
167 self.commands
168 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
169 direction: Direction::Row,
170 gap: 0,
171 align: Align::Start,
172 align_self: None,
173 justify: Justify::Start,
174 border: None,
175 border_sides: BorderSides::all(),
176 border_style: Style::new().fg(border),
177 bg_color: None,
178 padding: Padding::default(),
179 margin: Margin::default(),
180 constraints: Constraints::default(),
181 title: None,
182 grow: 0,
183 group_name: None,
184 })));
185
186 for (col_idx, element) in row.iter().enumerate() {
187 let spec = columns.get(col_idx).copied().unwrap_or(GridColumn::Auto);
188 let (grow, constraints) = match spec {
189 GridColumn::Auto => (1, Constraints::default()),
190 GridColumn::Fixed(w) => (0, Constraints::default().w(w)),
191 GridColumn::Grow(g) => (g, Constraints::default()),
192 GridColumn::Percent(p) => (0, Constraints::default().w_pct(p)),
193 };
194
195 self.skip_interaction_slot();
196 self.commands
197 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
198 direction: Direction::Column,
199 gap: 0,
200 align: Align::Start,
201 align_self: None,
202 justify: Justify::Start,
203 border: None,
204 border_sides: BorderSides::all(),
205 border_style: Style::new().fg(border),
206 bg_color: None,
207 padding: Padding::default(),
208 margin: Margin::default(),
209 constraints,
210 title: None,
211 grow,
212 group_name: None,
213 })));
214 self.commands.extend(element.iter().cloned());
215 self.commands.push(Command::EndContainer);
216 }
217
218 self.commands.push(Command::EndContainer);
219 }
220
221 self.commands.push(Command::EndContainer);
222 self.rollback.last_text_idx = None;
223
224 self.response_for(interaction_id)
225 }
226
227 pub fn list(&mut self, state: &mut ListState) -> Response {
232 let colors = self.widget_theme.list;
233 self.list_colored(state, &colors)
234 }
235
236 pub fn list_colored(&mut self, state: &mut ListState, colors: &WidgetColors) -> Response {
238 let visible = state.visible_indices().to_vec();
239 if visible.is_empty() && state.items.is_empty() {
240 state.selected = 0;
241 return Response::none();
242 }
243
244 if !visible.is_empty() {
245 state.selected = state.selected.min(visible.len().saturating_sub(1));
246 }
247
248 let old_selected = state.selected;
249 let focused = self.register_focusable();
250 let (interaction_id, mut response) = self.begin_widget_interaction(focused);
251
252 if focused {
253 let mut consumed_indices = Vec::new();
254 for (i, key) in self.available_key_presses() {
255 match key.code {
256 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
257 let _ = handle_vertical_nav(
258 &mut state.selected,
259 visible.len().saturating_sub(1),
260 key.code.clone(),
261 );
262 consumed_indices.push(i);
263 }
264 _ => {}
265 }
266 }
267 self.consume_indices(consumed_indices);
268 }
269
270 if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
271 let mut consumed = Vec::new();
272 for (i, mouse) in clicks {
273 let clicked_idx = (mouse.y - rect.y) as usize;
274 if clicked_idx < visible.len() {
275 state.selected = clicked_idx;
276 consumed.push(i);
277 }
278 }
279 self.consume_indices(consumed);
280 }
281
282 self.commands
283 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
284 direction: Direction::Column,
285 gap: 0,
286 align: Align::Start,
287 align_self: None,
288 justify: Justify::Start,
289 border: None,
290 border_sides: BorderSides::all(),
291 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
292 bg_color: None,
293 padding: Padding::default(),
294 margin: Margin::default(),
295 constraints: Constraints::default(),
296 title: None,
297 grow: 0,
298 group_name: None,
299 })));
300
301 for (view_idx, &item_idx) in visible.iter().enumerate() {
302 let item = &state.items[item_idx];
303 if view_idx == state.selected {
304 let mut selected_style = Style::new()
305 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
306 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
307 if focused {
308 selected_style = selected_style.bold();
309 }
310 let mut row = String::with_capacity(2 + item.len());
311 row.push_str("▸ ");
312 row.push_str(item);
313 self.styled(row, selected_style);
314 } else {
315 let mut row = String::with_capacity(2 + item.len());
316 row.push_str(" ");
317 row.push_str(item);
318 self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
319 }
320 }
321
322 self.commands.push(Command::EndContainer);
323 self.rollback.last_text_idx = None;
324
325 response.changed = state.selected != old_selected;
326 response
327 }
328
329 pub fn list_reorderable(&mut self, state: &mut ListState) -> crate::widgets::ListResponse {
360 let colors = self.widget_theme.list;
361 self.list_reorderable_colored(state, &colors)
362 }
363
364 pub fn list_reorderable_colored(
371 &mut self,
372 state: &mut ListState,
373 colors: &WidgetColors,
374 ) -> crate::widgets::ListResponse {
375 let visible = state.visible_indices().to_vec();
376 if visible.is_empty() && state.items.is_empty() {
377 state.selected = 0;
378 return crate::widgets::ListResponse::default();
379 }
380
381 if !visible.is_empty() {
382 state.selected = state.selected.min(visible.len().saturating_sub(1));
383 }
384
385 let old_selected = state.selected;
386 let focused = self.register_focusable();
387 let (interaction_id, mut response) = self.begin_widget_interaction(focused);
388
389 let mut reordered: Option<(usize, usize)> = None;
390
391 if focused {
392 let mut consumed_indices = Vec::new();
393 for (i, key) in self.available_key_presses() {
394 let modded = key.modifiers.contains(KeyModifiers::SHIFT)
397 || key.modifiers.contains(KeyModifiers::ALT);
398 let dir: Option<isize> = match key.code {
401 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => Some(-1),
402 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => Some(1),
403 _ => None,
404 };
405
406 if modded {
407 if let Some(delta) = dir {
408 let cur_view = state.selected;
409 let target_view = if delta < 0 {
410 cur_view.checked_sub(1)
411 } else {
412 let next = cur_view + 1;
413 (next < visible.len()).then_some(next)
414 };
415 if let Some(target_view) = target_view
418 && let (Some(&from), Some(&to)) =
419 (visible.get(cur_view), visible.get(target_view))
420 && state.move_item(from, to)
421 {
422 reordered = Some((from, to));
423 }
424 consumed_indices.push(i);
427 }
428 continue;
429 }
430
431 if dir.is_some() {
432 let _ = handle_vertical_nav(
433 &mut state.selected,
434 visible.len().saturating_sub(1),
435 key.code.clone(),
436 );
437 consumed_indices.push(i);
438 }
439 }
440 self.consume_indices(consumed_indices);
441 }
442
443 if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
444 let mut consumed = Vec::new();
445 let visible_len = state.visible_indices().len();
448 for (i, mouse) in clicks {
449 let clicked_idx = (mouse.y - rect.y) as usize;
450 if clicked_idx < visible_len {
451 state.selected = clicked_idx;
452 consumed.push(i);
453 }
454 }
455 self.consume_indices(consumed);
456 }
457
458 let visible = state.visible_indices().to_vec();
460
461 self.commands
462 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
463 direction: Direction::Column,
464 gap: 0,
465 align: Align::Start,
466 align_self: None,
467 justify: Justify::Start,
468 border: None,
469 border_sides: BorderSides::all(),
470 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
471 bg_color: None,
472 padding: Padding::default(),
473 margin: Margin::default(),
474 constraints: Constraints::default(),
475 title: None,
476 grow: 0,
477 group_name: None,
478 })));
479
480 for (view_idx, &item_idx) in visible.iter().enumerate() {
481 let item = &state.items[item_idx];
482 if view_idx == state.selected {
483 let mut selected_style = Style::new()
484 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
485 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
486 if focused {
487 selected_style = selected_style.bold();
488 }
489 let mut row = String::with_capacity(2 + item.len());
490 row.push_str("▸ ");
491 row.push_str(item);
492 self.styled(row, selected_style);
493 } else {
494 let mut row = String::with_capacity(2 + item.len());
495 row.push_str(" ");
496 row.push_str(item);
497 self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
498 }
499 }
500
501 self.commands.push(Command::EndContainer);
502 self.rollback.last_text_idx = None;
503
504 response.changed = state.selected != old_selected || reordered.is_some();
505 crate::widgets::ListResponse {
506 response,
507 reordered,
508 }
509 }
510
511 pub fn calendar(&mut self, state: &mut CalendarState) -> Response {
556 let focused = self.register_focusable();
557 let (interaction_id, mut response) = self.begin_widget_interaction(focused);
558
559 let month_days = CalendarState::days_in_month(state.year, state.month);
560 state.cursor_day = state.cursor_day.clamp(1, month_days);
561 if let Some(day) = state.selected_day {
562 state.selected_day = Some(day.min(month_days));
563 }
564 let old_selected = state.selected_day;
565 let old_anchor = state.anchor;
566 let old_extent = state.extent;
567 let old_time = (state.hour, state.minute);
568
569 if focused {
570 let mut consumed_indices = Vec::new();
571 for (i, key) in self.available_key_presses() {
572 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
573 let range = state.mode == CalendarSelect::Range;
574 let movement_delta = match key.code {
576 KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => Some(-1),
577 KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => Some(1),
578 KeyCode::Up => Some(-7),
579 KeyCode::Down => Some(7),
580 _ => None,
581 };
582
583 if let Some(delta) = movement_delta {
584 calendar_move_cursor_by_days(state, delta);
585 if range && shift {
586 state.extend_to_cursor();
588 }
589 consumed_indices.push(i);
590 continue;
591 }
592
593 match key.code {
594 KeyCode::Char('[') => {
595 state.prev_month();
596 consumed_indices.push(i);
597 }
598 KeyCode::Char(']') => {
599 state.next_month();
600 consumed_indices.push(i);
601 }
602 KeyCode::Enter | KeyCode::Char(' ') => {
603 if range {
604 if shift {
605 state.extend_to_cursor();
607 } else {
608 state.set_anchor_to_cursor();
610 }
611 } else {
612 state.selected_day = Some(state.cursor_day);
613 }
614 consumed_indices.push(i);
615 }
616 _ => {}
617 }
618 }
619 self.consume_indices(consumed_indices);
620 }
621
622 if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
623 let mut consumed = Vec::new();
624 for (i, mouse) in clicks {
625 let rel_x = mouse.x.saturating_sub(rect.x);
626 let rel_y = mouse.y.saturating_sub(rect.y);
627 if rel_y == 0 {
628 if rel_x <= 2 {
629 state.prev_month();
630 consumed.push(i);
631 continue;
632 }
633 if rel_x + 3 >= rect.width {
634 state.next_month();
635 consumed.push(i);
636 continue;
637 }
638 }
639
640 if !(2..8).contains(&rel_y) {
641 continue;
642 }
643 if rel_x >= 21 {
644 continue;
645 }
646
647 let week = rel_y - 2;
648 let col = rel_x / 3;
649 let day_index = week * 7 + col;
650 let first = CalendarState::first_weekday(state.year, state.month);
651 let days = CalendarState::days_in_month(state.year, state.month);
652 if day_index < first {
653 continue;
654 }
655 let day = day_index - first + 1;
656 if day == 0 || day > days {
657 continue;
658 }
659 state.cursor_day = day;
660 if state.mode == CalendarSelect::Range {
661 if mouse.modifiers.contains(KeyModifiers::SHIFT) {
662 state.extend_to_cursor();
664 } else {
665 state.set_anchor_to_cursor();
667 }
668 } else {
669 state.selected_day = Some(day);
670 }
671 consumed.push(i);
672 }
673 self.consume_indices(consumed);
674 }
675
676 let title = {
677 let month_name = calendar_month_name(state.month);
678 let mut s = String::with_capacity(16);
679 s.push_str(&state.year.to_string());
680 s.push(' ');
681 s.push_str(month_name);
682 s
683 };
684
685 self.commands
686 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
687 direction: Direction::Column,
688 gap: 0,
689 align: Align::Start,
690 align_self: None,
691 justify: Justify::Start,
692 border: None,
693 border_sides: BorderSides::all(),
694 border_style: Style::new().fg(self.theme.border),
695 bg_color: None,
696 padding: Padding::default(),
697 margin: Margin::default(),
698 constraints: Constraints::default(),
699 title: None,
700 grow: 0,
701 group_name: None,
702 })));
703
704 let cal_gap = self.theme.spacing.xs();
705 self.commands
706 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
707 direction: Direction::Row,
708 gap: cal_gap as i32,
709 align: Align::Start,
710 align_self: None,
711 justify: Justify::Start,
712 border: None,
713 border_sides: BorderSides::all(),
714 border_style: Style::new().fg(self.theme.border),
715 bg_color: None,
716 padding: Padding::default(),
717 margin: Margin::default(),
718 constraints: Constraints::default(),
719 title: None,
720 grow: 0,
721 group_name: None,
722 })));
723 self.styled("◀", Style::new().fg(self.theme.text));
724 self.styled(title, Style::new().bold().fg(self.theme.text));
725 self.styled("▶", Style::new().fg(self.theme.text));
726 self.commands.push(Command::EndContainer);
727
728 self.commands
729 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
730 direction: Direction::Row,
731 gap: 0,
732 align: Align::Start,
733 align_self: None,
734 justify: Justify::Start,
735 border: None,
736 border_sides: BorderSides::all(),
737 border_style: Style::new().fg(self.theme.border),
738 bg_color: None,
739 padding: Padding::default(),
740 margin: Margin::default(),
741 constraints: Constraints::default(),
742 title: None,
743 grow: 0,
744 group_name: None,
745 })));
746 for wd in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
747 self.styled(
748 format!("{wd:>2} "),
749 Style::new().fg(self.theme.text_dim).bold(),
750 );
751 }
752 self.commands.push(Command::EndContainer);
753
754 let first = CalendarState::first_weekday(state.year, state.month);
755 let days = CalendarState::days_in_month(state.year, state.month);
756 for week in 0..6_u32 {
757 self.commands
758 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
759 direction: Direction::Row,
760 gap: 0,
761 align: Align::Start,
762 align_self: None,
763 justify: Justify::Start,
764 border: None,
765 border_sides: BorderSides::all(),
766 border_style: Style::new().fg(self.theme.border),
767 bg_color: None,
768 padding: Padding::default(),
769 margin: Margin::default(),
770 constraints: Constraints::default(),
771 title: None,
772 grow: 0,
773 group_name: None,
774 })));
775
776 for col in 0..7_u32 {
777 let idx = week * 7 + col;
778 if idx < first || idx >= first + days {
779 self.styled(" ", Style::new().fg(self.theme.text_dim));
780 continue;
781 }
782 let day = idx - first + 1;
783 let text = format!("{day:>2} ");
784 let cell = CalDate {
785 year: state.year,
786 month: state.month,
787 day,
788 };
789 let style = if state.mode == CalendarSelect::Range {
790 if state.is_range_endpoint(cell) {
791 Style::new()
793 .bg(self.theme.selected_bg)
794 .fg(self.theme.selected_fg)
795 } else if state.in_range(cell) {
796 Style::new().bg(self.theme.surface).fg(self.theme.text)
798 } else if state.cursor_day == day {
799 Style::new().fg(self.theme.primary).bold()
800 } else {
801 Style::new().fg(self.theme.text)
802 }
803 } else if state.selected_day == Some(day) {
804 Style::new()
805 .bg(self.theme.selected_bg)
806 .fg(self.theme.selected_fg)
807 } else if state.cursor_day == day {
808 Style::new().fg(self.theme.primary).bold()
809 } else {
810 Style::new().fg(self.theme.text)
811 };
812 self.styled(text, style);
813 }
814
815 self.commands.push(Command::EndContainer);
816 }
817
818 if state.time_enabled {
819 let time_text = format!("{:02}:{:02}", state.hour, state.minute);
820 self.styled(time_text, Style::new().fg(self.theme.text).bold());
821 }
822
823 self.commands.push(Command::EndContainer);
824 self.rollback.last_text_idx = None;
825 response.changed = state.selected_day != old_selected
826 || state.anchor != old_anchor
827 || state.extent != old_extent
828 || (state.hour, state.minute) != old_time;
829 response
830 }
831
832 pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
834 if state.dirty {
835 state.refresh();
836 }
837 if !state.entries.is_empty() {
838 state.selected = state.selected.min(state.entries.len().saturating_sub(1));
839 }
840
841 let focused = self.register_focusable();
842 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
843 let mut file_selected = false;
844
845 if focused {
846 let mut consumed_indices = Vec::new();
847 for (i, key) in self.available_key_presses() {
848 match key.code {
849 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
850 if !state.entries.is_empty() {
851 let _ = handle_vertical_nav(
852 &mut state.selected,
853 state.entries.len().saturating_sub(1),
854 key.code.clone(),
855 );
856 }
857 consumed_indices.push(i);
858 }
859 KeyCode::Enter => {
860 if let Some(entry) = state.entries.get(state.selected).cloned() {
861 if entry.is_dir {
862 state.current_dir = entry.path;
863 state.selected = 0;
864 state.selected_file = None;
865 state.dirty = true;
866 } else {
867 state.selected_file = Some(entry.path);
868 file_selected = true;
869 }
870 }
871 consumed_indices.push(i);
872 }
873 KeyCode::Backspace => {
874 if let Some(parent) = state.current_dir.parent().map(|p| p.to_path_buf()) {
875 state.current_dir = parent;
876 state.selected = 0;
877 state.selected_file = None;
878 state.dirty = true;
879 }
880 consumed_indices.push(i);
881 }
882 KeyCode::Char('h') => {
883 state.show_hidden = !state.show_hidden;
884 state.selected = 0;
885 state.dirty = true;
886 consumed_indices.push(i);
887 }
888 KeyCode::Esc => {
889 state.selected_file = None;
890 consumed_indices.push(i);
891 }
892 _ => {}
893 }
894 }
895 self.consume_indices(consumed_indices);
896 }
897
898 if state.dirty {
899 state.refresh();
900 }
901
902 self.commands
903 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
904 direction: Direction::Column,
905 gap: 0,
906 align: Align::Start,
907 align_self: None,
908 justify: Justify::Start,
909 border: None,
910 border_sides: BorderSides::all(),
911 border_style: Style::new().fg(self.theme.border),
912 bg_color: None,
913 padding: Padding::default(),
914 margin: Margin::default(),
915 constraints: Constraints::default(),
916 title: None,
917 grow: 0,
918 group_name: None,
919 })));
920
921 let dir_text = {
922 let dir = state.current_dir.display().to_string();
923 let mut text = String::with_capacity(5 + dir.len());
924 text.push_str("Dir: ");
925 text.push_str(&dir);
926 text
927 };
928 self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
929
930 if state.entries.is_empty() {
931 self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
932 } else {
933 for (idx, entry) in state.entries.iter().enumerate() {
934 let icon = if entry.is_dir { "▸ " } else { " " };
935 let row = if entry.is_dir {
936 let mut row = String::with_capacity(icon.len() + entry.name.len());
937 row.push_str(icon);
938 row.push_str(&entry.name);
939 row
940 } else {
941 let size_text = entry.size.to_string();
942 let mut row =
943 String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
944 row.push_str(icon);
945 row.push_str(&entry.name);
946 row.push_str(" ");
947 row.push_str(&size_text);
948 row.push_str(" B");
949 row
950 };
951
952 let style = if idx == state.selected {
953 if focused {
954 Style::new().bold().fg(self.theme.primary)
955 } else {
956 Style::new().fg(self.theme.primary)
957 }
958 } else {
959 Style::new().fg(self.theme.text)
960 };
961 self.styled(row, style);
962 }
963 }
964
965 self.commands.push(Command::EndContainer);
966 self.rollback.last_text_idx = None;
967
968 response.changed = file_selected;
969 response
970 }
971}
972
973fn collect_grid_elements(child_commands: Vec<Command>) -> Vec<Vec<Command>> {
980 let mut elements: Vec<Vec<Command>> = Vec::new();
981 let mut iter = child_commands.into_iter().peekable();
982 let mut pending_markers: Vec<Command> = Vec::new();
983 while let Some(cmd) = iter.next() {
984 match cmd {
985 Command::InteractionMarker(_) => {
986 pending_markers.push(cmd);
987 }
988 Command::BeginContainer(_) | Command::BeginScrollable(_) => {
989 let mut depth = 1_u32;
990 let mut element: Vec<Command> = std::mem::take(&mut pending_markers);
991 element.push(cmd);
992 for next in iter.by_ref() {
993 match next {
994 Command::BeginContainer(_) | Command::BeginScrollable(_) => {
995 depth += 1;
996 }
997 Command::EndContainer => {
998 depth = depth.saturating_sub(1);
999 }
1000 _ => {}
1001 }
1002 let at_end = matches!(next, Command::EndContainer) && depth == 0;
1003 element.push(next);
1004 if at_end {
1005 break;
1006 }
1007 }
1008 elements.push(element);
1009 }
1010 Command::EndContainer => {}
1011 _ => {
1012 let mut element = std::mem::take(&mut pending_markers);
1013 element.push(cmd);
1014 elements.push(element);
1015 }
1016 }
1017 }
1018 if !pending_markers.is_empty() {
1020 elements.push(pending_markers);
1021 }
1022 elements
1023}
1024
1025#[cfg(test)]
1026mod list_reorder_render_tests {
1027 use crate::widgets::ListState;
1028 use crate::{EventBuilder, KeyCode, KeyModifiers, TestBackend};
1029
1030 #[test]
1031 fn shift_down_reorders_selected_item() {
1032 let mut backend = TestBackend::new(20, 6);
1033 let mut state = ListState::new(vec!["alpha", "beta", "gamma"]);
1034 state.selected = 0; let events = EventBuilder::new()
1037 .key_with(KeyCode::Down, KeyModifiers::SHIFT)
1038 .build();
1039
1040 let mut reordered = None;
1041 backend.run_with_events(events, |ui| {
1042 let r = ui.list_reorderable(&mut state);
1043 reordered = r.reordered;
1044 });
1045
1046 assert_eq!(reordered, Some((0, 1)));
1048 assert_eq!(state.items, vec!["beta", "alpha", "gamma"]);
1049 assert_eq!(state.selected, 1);
1051 assert_eq!(state.selected_item(), Some("alpha"));
1052 }
1053
1054 #[test]
1055 fn alt_up_reorders_selected_item() {
1056 let mut backend = TestBackend::new(20, 6);
1057 let mut state = ListState::new(vec!["one", "two", "three"]);
1058 state.selected = 2; let events = EventBuilder::new()
1061 .key_with(KeyCode::Up, KeyModifiers::ALT)
1062 .build();
1063
1064 let mut reordered = None;
1065 backend.run_with_events(events, |ui| {
1066 reordered = ui.list_reorderable(&mut state).reordered;
1067 });
1068
1069 assert_eq!(reordered, Some((2, 1)));
1070 assert_eq!(state.items, vec!["one", "three", "two"]);
1071 assert_eq!(state.selected, 1);
1072 }
1073
1074 #[test]
1075 fn shift_up_at_top_is_a_noop() {
1076 let mut backend = TestBackend::new(20, 6);
1077 let mut state = ListState::new(vec!["a", "b", "c"]);
1078 state.selected = 0;
1079
1080 let events = EventBuilder::new()
1081 .key_with(KeyCode::Up, KeyModifiers::SHIFT)
1082 .build();
1083
1084 let mut reordered = Some((9, 9));
1085 backend.run_with_events(events, |ui| {
1086 reordered = ui.list_reorderable(&mut state).reordered;
1087 });
1088
1089 assert_eq!(reordered, None);
1091 assert_eq!(state.items, vec!["a", "b", "c"]);
1092 assert_eq!(state.selected, 0);
1093 }
1094
1095 #[test]
1096 fn plain_down_navigates_without_reordering() {
1097 let mut backend = TestBackend::new(20, 6);
1098 let mut state = ListState::new(vec!["a", "b", "c"]);
1099 state.selected = 0;
1100
1101 let events = EventBuilder::new().key_code(KeyCode::Down).build();
1102
1103 let mut reordered = Some((9, 9));
1104 backend.run_with_events(events, |ui| {
1105 reordered = ui.list_reorderable(&mut state).reordered;
1106 });
1107
1108 assert_eq!(reordered, None);
1110 assert_eq!(state.items, vec!["a", "b", "c"]);
1111 assert_eq!(state.selected, 1);
1112 }
1113}