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 {
359 let colors = self.widget_theme.list;
360 self.list_reorderable_colored(state, &colors)
361 }
362
363 pub fn list_reorderable_colored(
370 &mut self,
371 state: &mut ListState,
372 colors: &WidgetColors,
373 ) -> crate::widgets::ListResponse {
374 let visible = state.visible_indices().to_vec();
375 if visible.is_empty() && state.items.is_empty() {
376 state.selected = 0;
377 return crate::widgets::ListResponse::default();
378 }
379
380 if !visible.is_empty() {
381 state.selected = state.selected.min(visible.len().saturating_sub(1));
382 }
383
384 let old_selected = state.selected;
385 let focused = self.register_focusable();
386 let (interaction_id, mut response) = self.begin_widget_interaction(focused);
387
388 let mut reordered: Option<(usize, usize)> = None;
389
390 if focused {
391 let mut consumed_indices = Vec::new();
392 for (i, key) in self.available_key_presses() {
393 let modded = key.modifiers.contains(KeyModifiers::SHIFT)
396 || key.modifiers.contains(KeyModifiers::ALT);
397 let dir: Option<isize> = match key.code {
400 KeyCode::Up | KeyCode::Char('k') | KeyCode::Char('K') => Some(-1),
401 KeyCode::Down | KeyCode::Char('j') | KeyCode::Char('J') => Some(1),
402 _ => None,
403 };
404
405 if modded {
406 if let Some(delta) = dir {
407 let cur_view = state.selected;
408 let target_view = if delta < 0 {
409 cur_view.checked_sub(1)
410 } else {
411 let next = cur_view + 1;
412 (next < visible.len()).then_some(next)
413 };
414 if let Some(target_view) = target_view {
417 if let (Some(&from), Some(&to)) =
418 (visible.get(cur_view), visible.get(target_view))
419 {
420 if state.move_item(from, to) {
421 reordered = Some((from, to));
422 }
423 }
424 }
425 consumed_indices.push(i);
428 }
429 continue;
430 }
431
432 if dir.is_some() {
433 let _ = handle_vertical_nav(
434 &mut state.selected,
435 visible.len().saturating_sub(1),
436 key.code.clone(),
437 );
438 consumed_indices.push(i);
439 }
440 }
441 self.consume_indices(consumed_indices);
442 }
443
444 if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
445 let mut consumed = Vec::new();
446 let visible_len = state.visible_indices().len();
449 for (i, mouse) in clicks {
450 let clicked_idx = (mouse.y - rect.y) as usize;
451 if clicked_idx < visible_len {
452 state.selected = clicked_idx;
453 consumed.push(i);
454 }
455 }
456 self.consume_indices(consumed);
457 }
458
459 let visible = state.visible_indices().to_vec();
461
462 self.commands
463 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
464 direction: Direction::Column,
465 gap: 0,
466 align: Align::Start,
467 align_self: None,
468 justify: Justify::Start,
469 border: None,
470 border_sides: BorderSides::all(),
471 border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
472 bg_color: None,
473 padding: Padding::default(),
474 margin: Margin::default(),
475 constraints: Constraints::default(),
476 title: None,
477 grow: 0,
478 group_name: None,
479 })));
480
481 for (view_idx, &item_idx) in visible.iter().enumerate() {
482 let item = &state.items[item_idx];
483 if view_idx == state.selected {
484 let mut selected_style = Style::new()
485 .bg(colors.accent.unwrap_or(self.theme.selected_bg))
486 .fg(colors.fg.unwrap_or(self.theme.selected_fg));
487 if focused {
488 selected_style = selected_style.bold();
489 }
490 let mut row = String::with_capacity(2 + item.len());
491 row.push_str("▸ ");
492 row.push_str(item);
493 self.styled(row, selected_style);
494 } else {
495 let mut row = String::with_capacity(2 + item.len());
496 row.push_str(" ");
497 row.push_str(item);
498 self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
499 }
500 }
501
502 self.commands.push(Command::EndContainer);
503 self.rollback.last_text_idx = None;
504
505 response.changed = state.selected != old_selected || reordered.is_some();
506 crate::widgets::ListResponse {
507 response,
508 reordered,
509 }
510 }
511
512 pub fn calendar(&mut self, state: &mut CalendarState) -> Response {
557 let focused = self.register_focusable();
558 let (interaction_id, mut response) = self.begin_widget_interaction(focused);
559
560 let month_days = CalendarState::days_in_month(state.year, state.month);
561 state.cursor_day = state.cursor_day.clamp(1, month_days);
562 if let Some(day) = state.selected_day {
563 state.selected_day = Some(day.min(month_days));
564 }
565 let old_selected = state.selected_day;
566 let old_anchor = state.anchor;
567 let old_extent = state.extent;
568 let old_time = (state.hour, state.minute);
569
570 if focused {
571 let mut consumed_indices = Vec::new();
572 for (i, key) in self.available_key_presses() {
573 let shift = key.modifiers.contains(KeyModifiers::SHIFT);
574 let range = state.mode == CalendarSelect::Range;
575 let movement_delta = match key.code {
577 KeyCode::Left | KeyCode::Char('h') | KeyCode::Char('H') => Some(-1),
578 KeyCode::Right | KeyCode::Char('l') | KeyCode::Char('L') => Some(1),
579 KeyCode::Up => Some(-7),
580 KeyCode::Down => Some(7),
581 _ => None,
582 };
583
584 if let Some(delta) = movement_delta {
585 calendar_move_cursor_by_days(state, delta);
586 if range && shift {
587 state.extend_to_cursor();
589 }
590 consumed_indices.push(i);
591 continue;
592 }
593
594 match key.code {
595 KeyCode::Char('[') => {
596 state.prev_month();
597 consumed_indices.push(i);
598 }
599 KeyCode::Char(']') => {
600 state.next_month();
601 consumed_indices.push(i);
602 }
603 KeyCode::Enter | KeyCode::Char(' ') => {
604 if range {
605 if shift {
606 state.extend_to_cursor();
608 } else {
609 state.set_anchor_to_cursor();
611 }
612 } else {
613 state.selected_day = Some(state.cursor_day);
614 }
615 consumed_indices.push(i);
616 }
617 _ => {}
618 }
619 }
620 self.consume_indices(consumed_indices);
621 }
622
623 if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
624 let mut consumed = Vec::new();
625 for (i, mouse) in clicks {
626 let rel_x = mouse.x.saturating_sub(rect.x);
627 let rel_y = mouse.y.saturating_sub(rect.y);
628 if rel_y == 0 {
629 if rel_x <= 2 {
630 state.prev_month();
631 consumed.push(i);
632 continue;
633 }
634 if rel_x + 3 >= rect.width {
635 state.next_month();
636 consumed.push(i);
637 continue;
638 }
639 }
640
641 if !(2..8).contains(&rel_y) {
642 continue;
643 }
644 if rel_x >= 21 {
645 continue;
646 }
647
648 let week = rel_y - 2;
649 let col = rel_x / 3;
650 let day_index = week * 7 + col;
651 let first = CalendarState::first_weekday(state.year, state.month);
652 let days = CalendarState::days_in_month(state.year, state.month);
653 if day_index < first {
654 continue;
655 }
656 let day = day_index - first + 1;
657 if day == 0 || day > days {
658 continue;
659 }
660 state.cursor_day = day;
661 if state.mode == CalendarSelect::Range {
662 if mouse.modifiers.contains(KeyModifiers::SHIFT) {
663 state.extend_to_cursor();
665 } else {
666 state.set_anchor_to_cursor();
668 }
669 } else {
670 state.selected_day = Some(day);
671 }
672 consumed.push(i);
673 }
674 self.consume_indices(consumed);
675 }
676
677 let title = {
678 let month_name = calendar_month_name(state.month);
679 let mut s = String::with_capacity(16);
680 s.push_str(&state.year.to_string());
681 s.push(' ');
682 s.push_str(month_name);
683 s
684 };
685
686 self.commands
687 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
688 direction: Direction::Column,
689 gap: 0,
690 align: Align::Start,
691 align_self: None,
692 justify: Justify::Start,
693 border: None,
694 border_sides: BorderSides::all(),
695 border_style: Style::new().fg(self.theme.border),
696 bg_color: None,
697 padding: Padding::default(),
698 margin: Margin::default(),
699 constraints: Constraints::default(),
700 title: None,
701 grow: 0,
702 group_name: None,
703 })));
704
705 let cal_gap = self.theme.spacing.xs();
706 self.commands
707 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
708 direction: Direction::Row,
709 gap: cal_gap as i32,
710 align: Align::Start,
711 align_self: None,
712 justify: Justify::Start,
713 border: None,
714 border_sides: BorderSides::all(),
715 border_style: Style::new().fg(self.theme.border),
716 bg_color: None,
717 padding: Padding::default(),
718 margin: Margin::default(),
719 constraints: Constraints::default(),
720 title: None,
721 grow: 0,
722 group_name: None,
723 })));
724 self.styled("◀", Style::new().fg(self.theme.text));
725 self.styled(title, Style::new().bold().fg(self.theme.text));
726 self.styled("▶", Style::new().fg(self.theme.text));
727 self.commands.push(Command::EndContainer);
728
729 self.commands
730 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
731 direction: Direction::Row,
732 gap: 0,
733 align: Align::Start,
734 align_self: None,
735 justify: Justify::Start,
736 border: None,
737 border_sides: BorderSides::all(),
738 border_style: Style::new().fg(self.theme.border),
739 bg_color: None,
740 padding: Padding::default(),
741 margin: Margin::default(),
742 constraints: Constraints::default(),
743 title: None,
744 grow: 0,
745 group_name: None,
746 })));
747 for wd in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
748 self.styled(
749 format!("{wd:>2} "),
750 Style::new().fg(self.theme.text_dim).bold(),
751 );
752 }
753 self.commands.push(Command::EndContainer);
754
755 let first = CalendarState::first_weekday(state.year, state.month);
756 let days = CalendarState::days_in_month(state.year, state.month);
757 for week in 0..6_u32 {
758 self.commands
759 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
760 direction: Direction::Row,
761 gap: 0,
762 align: Align::Start,
763 align_self: None,
764 justify: Justify::Start,
765 border: None,
766 border_sides: BorderSides::all(),
767 border_style: Style::new().fg(self.theme.border),
768 bg_color: None,
769 padding: Padding::default(),
770 margin: Margin::default(),
771 constraints: Constraints::default(),
772 title: None,
773 grow: 0,
774 group_name: None,
775 })));
776
777 for col in 0..7_u32 {
778 let idx = week * 7 + col;
779 if idx < first || idx >= first + days {
780 self.styled(" ", Style::new().fg(self.theme.text_dim));
781 continue;
782 }
783 let day = idx - first + 1;
784 let text = format!("{day:>2} ");
785 let cell = CalDate {
786 year: state.year,
787 month: state.month,
788 day,
789 };
790 let style = if state.mode == CalendarSelect::Range {
791 if state.is_range_endpoint(cell) {
792 Style::new()
794 .bg(self.theme.selected_bg)
795 .fg(self.theme.selected_fg)
796 } else if state.in_range(cell) {
797 Style::new().bg(self.theme.surface).fg(self.theme.text)
799 } else if state.cursor_day == day {
800 Style::new().fg(self.theme.primary).bold()
801 } else {
802 Style::new().fg(self.theme.text)
803 }
804 } else if state.selected_day == Some(day) {
805 Style::new()
806 .bg(self.theme.selected_bg)
807 .fg(self.theme.selected_fg)
808 } else if state.cursor_day == day {
809 Style::new().fg(self.theme.primary).bold()
810 } else {
811 Style::new().fg(self.theme.text)
812 };
813 self.styled(text, style);
814 }
815
816 self.commands.push(Command::EndContainer);
817 }
818
819 if state.time_enabled {
820 let time_text = format!("{:02}:{:02}", state.hour, state.minute);
821 self.styled(time_text, Style::new().fg(self.theme.text).bold());
822 }
823
824 self.commands.push(Command::EndContainer);
825 self.rollback.last_text_idx = None;
826 response.changed = state.selected_day != old_selected
827 || state.anchor != old_anchor
828 || state.extent != old_extent
829 || (state.hour, state.minute) != old_time;
830 response
831 }
832
833 pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
835 if state.dirty {
836 state.refresh();
837 }
838 if !state.entries.is_empty() {
839 state.selected = state.selected.min(state.entries.len().saturating_sub(1));
840 }
841
842 let focused = self.register_focusable();
843 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
844 let mut file_selected = false;
845
846 if focused {
847 let mut consumed_indices = Vec::new();
848 for (i, key) in self.available_key_presses() {
849 match key.code {
850 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
851 if !state.entries.is_empty() {
852 let _ = handle_vertical_nav(
853 &mut state.selected,
854 state.entries.len().saturating_sub(1),
855 key.code.clone(),
856 );
857 }
858 consumed_indices.push(i);
859 }
860 KeyCode::Enter => {
861 if let Some(entry) = state.entries.get(state.selected).cloned() {
862 if entry.is_dir {
863 state.current_dir = entry.path;
864 state.selected = 0;
865 state.selected_file = None;
866 state.dirty = true;
867 } else {
868 state.selected_file = Some(entry.path);
869 file_selected = true;
870 }
871 }
872 consumed_indices.push(i);
873 }
874 KeyCode::Backspace => {
875 if let Some(parent) = state.current_dir.parent().map(|p| p.to_path_buf()) {
876 state.current_dir = parent;
877 state.selected = 0;
878 state.selected_file = None;
879 state.dirty = true;
880 }
881 consumed_indices.push(i);
882 }
883 KeyCode::Char('h') => {
884 state.show_hidden = !state.show_hidden;
885 state.selected = 0;
886 state.dirty = true;
887 consumed_indices.push(i);
888 }
889 KeyCode::Esc => {
890 state.selected_file = None;
891 consumed_indices.push(i);
892 }
893 _ => {}
894 }
895 }
896 self.consume_indices(consumed_indices);
897 }
898
899 if state.dirty {
900 state.refresh();
901 }
902
903 self.commands
904 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
905 direction: Direction::Column,
906 gap: 0,
907 align: Align::Start,
908 align_self: None,
909 justify: Justify::Start,
910 border: None,
911 border_sides: BorderSides::all(),
912 border_style: Style::new().fg(self.theme.border),
913 bg_color: None,
914 padding: Padding::default(),
915 margin: Margin::default(),
916 constraints: Constraints::default(),
917 title: None,
918 grow: 0,
919 group_name: None,
920 })));
921
922 let dir_text = {
923 let dir = state.current_dir.display().to_string();
924 let mut text = String::with_capacity(5 + dir.len());
925 text.push_str("Dir: ");
926 text.push_str(&dir);
927 text
928 };
929 self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
930
931 if state.entries.is_empty() {
932 self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
933 } else {
934 for (idx, entry) in state.entries.iter().enumerate() {
935 let icon = if entry.is_dir { "▸ " } else { " " };
936 let row = if entry.is_dir {
937 let mut row = String::with_capacity(icon.len() + entry.name.len());
938 row.push_str(icon);
939 row.push_str(&entry.name);
940 row
941 } else {
942 let size_text = entry.size.to_string();
943 let mut row =
944 String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
945 row.push_str(icon);
946 row.push_str(&entry.name);
947 row.push_str(" ");
948 row.push_str(&size_text);
949 row.push_str(" B");
950 row
951 };
952
953 let style = if idx == state.selected {
954 if focused {
955 Style::new().bold().fg(self.theme.primary)
956 } else {
957 Style::new().fg(self.theme.primary)
958 }
959 } else {
960 Style::new().fg(self.theme.text)
961 };
962 self.styled(row, style);
963 }
964 }
965
966 self.commands.push(Command::EndContainer);
967 self.rollback.last_text_idx = None;
968
969 response.changed = file_selected;
970 response
971 }
972}
973
974fn collect_grid_elements(child_commands: Vec<Command>) -> Vec<Vec<Command>> {
981 let mut elements: Vec<Vec<Command>> = Vec::new();
982 let mut iter = child_commands.into_iter().peekable();
983 let mut pending_markers: Vec<Command> = Vec::new();
984 while let Some(cmd) = iter.next() {
985 match cmd {
986 Command::InteractionMarker(_) => {
987 pending_markers.push(cmd);
988 }
989 Command::BeginContainer(_) | Command::BeginScrollable(_) => {
990 let mut depth = 1_u32;
991 let mut element: Vec<Command> = std::mem::take(&mut pending_markers);
992 element.push(cmd);
993 for next in iter.by_ref() {
994 match next {
995 Command::BeginContainer(_) | Command::BeginScrollable(_) => {
996 depth += 1;
997 }
998 Command::EndContainer => {
999 depth = depth.saturating_sub(1);
1000 }
1001 _ => {}
1002 }
1003 let at_end = matches!(next, Command::EndContainer) && depth == 0;
1004 element.push(next);
1005 if at_end {
1006 break;
1007 }
1008 }
1009 elements.push(element);
1010 }
1011 Command::EndContainer => {}
1012 _ => {
1013 let mut element = std::mem::take(&mut pending_markers);
1014 element.push(cmd);
1015 elements.push(element);
1016 }
1017 }
1018 }
1019 if !pending_markers.is_empty() {
1021 elements.push(pending_markers);
1022 }
1023 elements
1024}
1025
1026#[cfg(test)]
1027mod list_reorder_render_tests {
1028 use crate::widgets::ListState;
1029 use crate::{EventBuilder, KeyCode, KeyModifiers, TestBackend};
1030
1031 #[test]
1032 fn shift_down_reorders_selected_item() {
1033 let mut backend = TestBackend::new(20, 6);
1034 let mut state = ListState::new(vec!["alpha", "beta", "gamma"]);
1035 state.selected = 0; let events = EventBuilder::new()
1038 .key_with(KeyCode::Down, KeyModifiers::SHIFT)
1039 .build();
1040
1041 let mut reordered = None;
1042 backend.run_with_events(events, |ui| {
1043 let r = ui.list_reorderable(&mut state);
1044 reordered = r.reordered;
1045 });
1046
1047 assert_eq!(reordered, Some((0, 1)));
1049 assert_eq!(state.items, vec!["beta", "alpha", "gamma"]);
1050 assert_eq!(state.selected, 1);
1052 assert_eq!(state.selected_item(), Some("alpha"));
1053 }
1054
1055 #[test]
1056 fn alt_up_reorders_selected_item() {
1057 let mut backend = TestBackend::new(20, 6);
1058 let mut state = ListState::new(vec!["one", "two", "three"]);
1059 state.selected = 2; let events = EventBuilder::new()
1062 .key_with(KeyCode::Up, KeyModifiers::ALT)
1063 .build();
1064
1065 let mut reordered = None;
1066 backend.run_with_events(events, |ui| {
1067 reordered = ui.list_reorderable(&mut state).reordered;
1068 });
1069
1070 assert_eq!(reordered, Some((2, 1)));
1071 assert_eq!(state.items, vec!["one", "three", "two"]);
1072 assert_eq!(state.selected, 1);
1073 }
1074
1075 #[test]
1076 fn shift_up_at_top_is_a_noop() {
1077 let mut backend = TestBackend::new(20, 6);
1078 let mut state = ListState::new(vec!["a", "b", "c"]);
1079 state.selected = 0;
1080
1081 let events = EventBuilder::new()
1082 .key_with(KeyCode::Up, KeyModifiers::SHIFT)
1083 .build();
1084
1085 let mut reordered = Some((9, 9));
1086 backend.run_with_events(events, |ui| {
1087 reordered = ui.list_reorderable(&mut state).reordered;
1088 });
1089
1090 assert_eq!(reordered, None);
1092 assert_eq!(state.items, vec!["a", "b", "c"]);
1093 assert_eq!(state.selected, 0);
1094 }
1095
1096 #[test]
1097 fn plain_down_navigates_without_reordering() {
1098 let mut backend = TestBackend::new(20, 6);
1099 let mut state = ListState::new(vec!["a", "b", "c"]);
1100 state.selected = 0;
1101
1102 let events = EventBuilder::new().key_code(KeyCode::Down).build();
1103
1104 let mut reordered = Some((9, 9));
1105 backend.run_with_events(events, |ui| {
1106 reordered = ui.list_reorderable(&mut state).reordered;
1107 });
1108
1109 assert_eq!(reordered, None);
1111 assert_eq!(state.items, vec!["a", "b", "c"]);
1112 assert_eq!(state.selected, 1);
1113 }
1114}