Skip to main content

slt/context/widgets_interactive/
collections.rs

1use super::*;
2
3impl Context {
4    /// Render children in a fixed grid with the given number of columns.
5    ///
6    /// Children are placed left-to-right, top-to-bottom. Each cell has equal
7    /// width (`area_width / cols`). Rows wrap automatically.
8    ///
9    /// # Example
10    ///
11    /// ```no_run
12    /// # slt::run(|ui: &mut slt::Context| {
13    /// ui.grid(3, |ui| {
14    ///     for i in 0..9 {
15    ///         ui.text(format!("Cell {i}"));
16    ///     }
17    /// });
18    /// # });
19    /// ```
20    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 mut elements: Vec<Vec<Command>> = Vec::new();
49        let mut iter = child_commands.into_iter().peekable();
50        let mut pending_markers: Vec<Command> = Vec::new();
51        while let Some(cmd) = iter.next() {
52            match cmd {
53                Command::InteractionMarker(_) => {
54                    pending_markers.push(cmd);
55                }
56                Command::BeginContainer(_) | Command::BeginScrollable(_) => {
57                    let mut depth = 1_u32;
58                    let mut element: Vec<Command> = std::mem::take(&mut pending_markers);
59                    element.push(cmd);
60                    for next in iter.by_ref() {
61                        match next {
62                            Command::BeginContainer(_) | Command::BeginScrollable(_) => {
63                                depth += 1;
64                            }
65                            Command::EndContainer => {
66                                depth = depth.saturating_sub(1);
67                            }
68                            _ => {}
69                        }
70                        let at_end = matches!(next, Command::EndContainer) && depth == 0;
71                        element.push(next);
72                        if at_end {
73                            break;
74                        }
75                    }
76                    elements.push(element);
77                }
78                Command::EndContainer => {}
79                _ => {
80                    let mut element = std::mem::take(&mut pending_markers);
81                    element.push(cmd);
82                    elements.push(element);
83                }
84            }
85        }
86        // Flush any trailing markers (edge case: marker with no following command)
87        if !pending_markers.is_empty() {
88            elements.push(pending_markers);
89        }
90
91        let cols = cols.max(1) as usize;
92        for row in elements.chunks(cols) {
93            self.skip_interaction_slot();
94            self.commands
95                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
96                    direction: Direction::Row,
97                    gap: 0,
98                    align: Align::Start,
99                    align_self: None,
100                    justify: Justify::Start,
101                    border: None,
102                    border_sides: BorderSides::all(),
103                    border_style: Style::new().fg(border),
104                    bg_color: None,
105                    padding: Padding::default(),
106                    margin: Margin::default(),
107                    constraints: Constraints::default(),
108                    title: None,
109                    grow: 0,
110                    group_name: None,
111                })));
112
113            for element in row {
114                self.skip_interaction_slot();
115                self.commands
116                    .push(Command::BeginContainer(Box::new(BeginContainerArgs {
117                        direction: Direction::Column,
118                        gap: 0,
119                        align: Align::Start,
120                        align_self: None,
121                        justify: Justify::Start,
122                        border: None,
123                        border_sides: BorderSides::all(),
124                        border_style: Style::new().fg(border),
125                        bg_color: None,
126                        padding: Padding::default(),
127                        margin: Margin::default(),
128                        constraints: Constraints::default(),
129                        title: None,
130                        grow: 1,
131                        group_name: None,
132                    })));
133                self.commands.extend(element.iter().cloned());
134                self.commands.push(Command::EndContainer);
135            }
136
137            self.commands.push(Command::EndContainer);
138        }
139
140        self.commands.push(Command::EndContainer);
141        self.rollback.last_text_idx = None;
142
143        self.response_for(interaction_id)
144    }
145
146    /// Render children in a grid with per-column width specifications.
147    ///
148    /// The number of columns is determined by the length of `columns`. Children
149    /// are placed left-to-right, top-to-bottom, wrapping into rows
150    /// automatically.
151    ///
152    /// # Column specifications
153    ///
154    /// - [`GridColumn::Auto`] — equal-width flex column (same as `grid()`)
155    /// - [`GridColumn::Fixed(n)`](GridColumn::Fixed) — exactly `n` character cells wide
156    /// - [`GridColumn::Grow(w)`](GridColumn::Grow) — flexible with grow weight `w`
157    /// - [`GridColumn::Percent(p)`](GridColumn::Percent) — `p`% of the grid width
158    ///
159    /// # Example
160    ///
161    /// ```no_run
162    /// use slt::GridColumn;
163    /// # slt::run(|ui: &mut slt::Context| {
164    /// ui.grid_with(&[
165    ///     GridColumn::Fixed(8),
166    ///     GridColumn::Grow(1),
167    ///     GridColumn::Grow(1),
168    ///     GridColumn::Fixed(4),
169    /// ], |ui| {
170    ///     for i in 0..8 {
171    ///         ui.text(format!("Cell {i}"));
172    ///     }
173    /// });
174    /// # });
175    /// ```
176    pub fn grid_with(&mut self, columns: &[GridColumn], f: impl FnOnce(&mut Context)) -> Response {
177        let cols = columns.len().max(1);
178        let interaction_id = self.next_interaction_id();
179        let border = self.theme.border;
180
181        self.commands
182            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
183                direction: Direction::Column,
184                gap: 0,
185                align: Align::Start,
186                align_self: None,
187                justify: Justify::Start,
188                border: None,
189                border_sides: BorderSides::all(),
190                border_style: Style::new().fg(border),
191                bg_color: None,
192                padding: Padding::default(),
193                margin: Margin::default(),
194                constraints: Constraints::default(),
195                title: None,
196                grow: 0,
197                group_name: None,
198            })));
199
200        let children_start = self.commands.len();
201        f(self);
202        let child_commands: Vec<Command> = self.commands.drain(children_start..).collect();
203
204        let mut elements: Vec<Vec<Command>> = Vec::new();
205        let mut iter = child_commands.into_iter().peekable();
206        let mut pending_markers: Vec<Command> = Vec::new();
207        while let Some(cmd) = iter.next() {
208            match cmd {
209                Command::InteractionMarker(_) => {
210                    pending_markers.push(cmd);
211                }
212                Command::BeginContainer(_) | Command::BeginScrollable(_) => {
213                    let mut depth = 1_u32;
214                    let mut element: Vec<Command> = std::mem::take(&mut pending_markers);
215                    element.push(cmd);
216                    for next in iter.by_ref() {
217                        match next {
218                            Command::BeginContainer(_) | Command::BeginScrollable(_) => {
219                                depth += 1;
220                            }
221                            Command::EndContainer => {
222                                depth = depth.saturating_sub(1);
223                            }
224                            _ => {}
225                        }
226                        let at_end = matches!(next, Command::EndContainer) && depth == 0;
227                        element.push(next);
228                        if at_end {
229                            break;
230                        }
231                    }
232                    elements.push(element);
233                }
234                Command::EndContainer => {}
235                _ => {
236                    let mut element = std::mem::take(&mut pending_markers);
237                    element.push(cmd);
238                    elements.push(element);
239                }
240            }
241        }
242        if !pending_markers.is_empty() {
243            elements.push(pending_markers);
244        }
245
246        for row in elements.chunks(cols) {
247            self.skip_interaction_slot();
248            self.commands
249                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
250                    direction: Direction::Row,
251                    gap: 0,
252                    align: Align::Start,
253                    align_self: None,
254                    justify: Justify::Start,
255                    border: None,
256                    border_sides: BorderSides::all(),
257                    border_style: Style::new().fg(border),
258                    bg_color: None,
259                    padding: Padding::default(),
260                    margin: Margin::default(),
261                    constraints: Constraints::default(),
262                    title: None,
263                    grow: 0,
264                    group_name: None,
265                })));
266
267            for (col_idx, element) in row.iter().enumerate() {
268                let spec = columns.get(col_idx).copied().unwrap_or(GridColumn::Auto);
269                let (grow, constraints) = match spec {
270                    GridColumn::Auto => (1, Constraints::default()),
271                    GridColumn::Fixed(w) => (
272                        0,
273                        Constraints {
274                            min_width: Some(w),
275                            max_width: Some(w),
276                            ..Constraints::default()
277                        },
278                    ),
279                    GridColumn::Grow(g) => (g, Constraints::default()),
280                    GridColumn::Percent(p) => (
281                        0,
282                        Constraints {
283                            width_pct: Some(p),
284                            ..Constraints::default()
285                        },
286                    ),
287                };
288
289                self.skip_interaction_slot();
290                self.commands
291                    .push(Command::BeginContainer(Box::new(BeginContainerArgs {
292                        direction: Direction::Column,
293                        gap: 0,
294                        align: Align::Start,
295                        align_self: None,
296                        justify: Justify::Start,
297                        border: None,
298                        border_sides: BorderSides::all(),
299                        border_style: Style::new().fg(border),
300                        bg_color: None,
301                        padding: Padding::default(),
302                        margin: Margin::default(),
303                        constraints,
304                        title: None,
305                        grow,
306                        group_name: None,
307                    })));
308                self.commands.extend(element.iter().cloned());
309                self.commands.push(Command::EndContainer);
310            }
311
312            self.commands.push(Command::EndContainer);
313        }
314
315        self.commands.push(Command::EndContainer);
316        self.rollback.last_text_idx = None;
317
318        self.response_for(interaction_id)
319    }
320
321    /// Render a selectable list. Handles Up/Down (and `k`/`j`) navigation when focused.
322    ///
323    /// The selected item is highlighted with the theme's primary color. If the
324    /// list is empty, nothing is rendered.
325    /// Render a navigable list widget.
326    pub fn list(&mut self, state: &mut ListState) -> Response {
327        let colors = self.widget_theme.list;
328        self.list_colored(state, &colors)
329    }
330
331    /// Render a navigable list with custom widget colors.
332    pub fn list_colored(&mut self, state: &mut ListState, colors: &WidgetColors) -> Response {
333        let visible = state.visible_indices().to_vec();
334        if visible.is_empty() && state.items.is_empty() {
335            state.selected = 0;
336            return Response::none();
337        }
338
339        if !visible.is_empty() {
340            state.selected = state.selected.min(visible.len().saturating_sub(1));
341        }
342
343        let old_selected = state.selected;
344        let focused = self.register_focusable();
345        let (interaction_id, mut response) = self.begin_widget_interaction(focused);
346
347        if focused {
348            let mut consumed_indices = Vec::new();
349            for (i, key) in self.available_key_presses() {
350                match key.code {
351                    KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
352                        let _ = handle_vertical_nav(
353                            &mut state.selected,
354                            visible.len().saturating_sub(1),
355                            key.code.clone(),
356                        );
357                        consumed_indices.push(i);
358                    }
359                    _ => {}
360                }
361            }
362            self.consume_indices(consumed_indices);
363        }
364
365        if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
366            let mut consumed = Vec::new();
367            for (i, mouse) in clicks {
368                let clicked_idx = (mouse.y - rect.y) as usize;
369                if clicked_idx < visible.len() {
370                    state.selected = clicked_idx;
371                    consumed.push(i);
372                }
373            }
374            self.consume_indices(consumed);
375        }
376
377        self.commands
378            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
379                direction: Direction::Column,
380                gap: 0,
381                align: Align::Start,
382                align_self: None,
383                justify: Justify::Start,
384                border: None,
385                border_sides: BorderSides::all(),
386                border_style: Style::new().fg(colors.border.unwrap_or(self.theme.border)),
387                bg_color: None,
388                padding: Padding::default(),
389                margin: Margin::default(),
390                constraints: Constraints::default(),
391                title: None,
392                grow: 0,
393                group_name: None,
394            })));
395
396        for (view_idx, &item_idx) in visible.iter().enumerate() {
397            let item = &state.items[item_idx];
398            if view_idx == state.selected {
399                let mut selected_style = Style::new()
400                    .bg(colors.accent.unwrap_or(self.theme.selected_bg))
401                    .fg(colors.fg.unwrap_or(self.theme.selected_fg));
402                if focused {
403                    selected_style = selected_style.bold();
404                }
405                let mut row = String::with_capacity(2 + item.len());
406                row.push_str("▸ ");
407                row.push_str(item);
408                self.styled(row, selected_style);
409            } else {
410                let mut row = String::with_capacity(2 + item.len());
411                row.push_str("  ");
412                row.push_str(item);
413                self.styled(row, Style::new().fg(colors.fg.unwrap_or(self.theme.text)));
414            }
415        }
416
417        self.commands.push(Command::EndContainer);
418        self.rollback.last_text_idx = None;
419
420        response.changed = state.selected != old_selected;
421        response
422    }
423
424    /// Render a calendar date picker with month navigation.
425    pub fn calendar(&mut self, state: &mut CalendarState) -> Response {
426        let focused = self.register_focusable();
427        let (interaction_id, mut response) = self.begin_widget_interaction(focused);
428
429        let month_days = CalendarState::days_in_month(state.year, state.month);
430        state.cursor_day = state.cursor_day.clamp(1, month_days);
431        if let Some(day) = state.selected_day {
432            state.selected_day = Some(day.min(month_days));
433        }
434        let old_selected = state.selected_day;
435
436        if focused {
437            let mut consumed_indices = Vec::new();
438            for (i, key) in self.available_key_presses() {
439                match key.code {
440                    KeyCode::Left => {
441                        calendar_move_cursor_by_days(state, -1);
442                        consumed_indices.push(i);
443                    }
444                    KeyCode::Right => {
445                        calendar_move_cursor_by_days(state, 1);
446                        consumed_indices.push(i);
447                    }
448                    KeyCode::Up => {
449                        calendar_move_cursor_by_days(state, -7);
450                        consumed_indices.push(i);
451                    }
452                    KeyCode::Down => {
453                        calendar_move_cursor_by_days(state, 7);
454                        consumed_indices.push(i);
455                    }
456                    KeyCode::Char('h') => {
457                        state.prev_month();
458                        consumed_indices.push(i);
459                    }
460                    KeyCode::Char('l') => {
461                        state.next_month();
462                        consumed_indices.push(i);
463                    }
464                    KeyCode::Enter | KeyCode::Char(' ') => {
465                        state.selected_day = Some(state.cursor_day);
466                        consumed_indices.push(i);
467                    }
468                    _ => {}
469                }
470            }
471            self.consume_indices(consumed_indices);
472        }
473
474        if let Some((rect, clicks)) = self.left_clicks_for_interaction(interaction_id) {
475            let mut consumed = Vec::new();
476            for (i, mouse) in clicks {
477                let rel_x = mouse.x.saturating_sub(rect.x);
478                let rel_y = mouse.y.saturating_sub(rect.y);
479                if rel_y == 0 {
480                    if rel_x <= 2 {
481                        state.prev_month();
482                        consumed.push(i);
483                        continue;
484                    }
485                    if rel_x + 3 >= rect.width {
486                        state.next_month();
487                        consumed.push(i);
488                        continue;
489                    }
490                }
491
492                if !(2..8).contains(&rel_y) {
493                    continue;
494                }
495                if rel_x >= 21 {
496                    continue;
497                }
498
499                let week = rel_y - 2;
500                let col = rel_x / 3;
501                let day_index = week * 7 + col;
502                let first = CalendarState::first_weekday(state.year, state.month);
503                let days = CalendarState::days_in_month(state.year, state.month);
504                if day_index < first {
505                    continue;
506                }
507                let day = day_index - first + 1;
508                if day == 0 || day > days {
509                    continue;
510                }
511                state.cursor_day = day;
512                state.selected_day = Some(day);
513                consumed.push(i);
514            }
515            self.consume_indices(consumed);
516        }
517
518        let title = {
519            let month_name = calendar_month_name(state.month);
520            let mut s = String::with_capacity(16);
521            s.push_str(&state.year.to_string());
522            s.push(' ');
523            s.push_str(month_name);
524            s
525        };
526
527        self.commands
528            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
529                direction: Direction::Column,
530                gap: 0,
531                align: Align::Start,
532                align_self: None,
533                justify: Justify::Start,
534                border: None,
535                border_sides: BorderSides::all(),
536                border_style: Style::new().fg(self.theme.border),
537                bg_color: None,
538                padding: Padding::default(),
539                margin: Margin::default(),
540                constraints: Constraints::default(),
541                title: None,
542                grow: 0,
543                group_name: None,
544            })));
545
546        self.commands
547            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
548                direction: Direction::Row,
549                gap: 1,
550                align: Align::Start,
551                align_self: None,
552                justify: Justify::Start,
553                border: None,
554                border_sides: BorderSides::all(),
555                border_style: Style::new().fg(self.theme.border),
556                bg_color: None,
557                padding: Padding::default(),
558                margin: Margin::default(),
559                constraints: Constraints::default(),
560                title: None,
561                grow: 0,
562                group_name: None,
563            })));
564        self.styled("◀", Style::new().fg(self.theme.text));
565        self.styled(title, Style::new().bold().fg(self.theme.text));
566        self.styled("▶", Style::new().fg(self.theme.text));
567        self.commands.push(Command::EndContainer);
568
569        self.commands
570            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
571                direction: Direction::Row,
572                gap: 0,
573                align: Align::Start,
574                align_self: None,
575                justify: Justify::Start,
576                border: None,
577                border_sides: BorderSides::all(),
578                border_style: Style::new().fg(self.theme.border),
579                bg_color: None,
580                padding: Padding::default(),
581                margin: Margin::default(),
582                constraints: Constraints::default(),
583                title: None,
584                grow: 0,
585                group_name: None,
586            })));
587        for wd in ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"] {
588            self.styled(
589                format!("{wd:>2} "),
590                Style::new().fg(self.theme.text_dim).bold(),
591            );
592        }
593        self.commands.push(Command::EndContainer);
594
595        let first = CalendarState::first_weekday(state.year, state.month);
596        let days = CalendarState::days_in_month(state.year, state.month);
597        for week in 0..6_u32 {
598            self.commands
599                .push(Command::BeginContainer(Box::new(BeginContainerArgs {
600                    direction: Direction::Row,
601                    gap: 0,
602                    align: Align::Start,
603                    align_self: None,
604                    justify: Justify::Start,
605                    border: None,
606                    border_sides: BorderSides::all(),
607                    border_style: Style::new().fg(self.theme.border),
608                    bg_color: None,
609                    padding: Padding::default(),
610                    margin: Margin::default(),
611                    constraints: Constraints::default(),
612                    title: None,
613                    grow: 0,
614                    group_name: None,
615                })));
616
617            for col in 0..7_u32 {
618                let idx = week * 7 + col;
619                if idx < first || idx >= first + days {
620                    self.styled("   ", Style::new().fg(self.theme.text_dim));
621                    continue;
622                }
623                let day = idx - first + 1;
624                let text = format!("{day:>2} ");
625                let style = if state.selected_day == Some(day) {
626                    Style::new()
627                        .bg(self.theme.selected_bg)
628                        .fg(self.theme.selected_fg)
629                } else if state.cursor_day == day {
630                    Style::new().fg(self.theme.primary).bold()
631                } else {
632                    Style::new().fg(self.theme.text)
633                };
634                self.styled(text, style);
635            }
636
637            self.commands.push(Command::EndContainer);
638        }
639
640        self.commands.push(Command::EndContainer);
641        self.rollback.last_text_idx = None;
642        response.changed = state.selected_day != old_selected;
643        response
644    }
645
646    /// Render a file system browser with directory navigation.
647    pub fn file_picker(&mut self, state: &mut FilePickerState) -> Response {
648        if state.dirty {
649            state.refresh();
650        }
651        if !state.entries.is_empty() {
652            state.selected = state.selected.min(state.entries.len().saturating_sub(1));
653        }
654
655        let focused = self.register_focusable();
656        let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
657        let mut file_selected = false;
658
659        if focused {
660            let mut consumed_indices = Vec::new();
661            for (i, key) in self.available_key_presses() {
662                match key.code {
663                    KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
664                        if !state.entries.is_empty() {
665                            let _ = handle_vertical_nav(
666                                &mut state.selected,
667                                state.entries.len().saturating_sub(1),
668                                key.code.clone(),
669                            );
670                        }
671                        consumed_indices.push(i);
672                    }
673                    KeyCode::Enter => {
674                        if let Some(entry) = state.entries.get(state.selected).cloned() {
675                            if entry.is_dir {
676                                state.current_dir = entry.path;
677                                state.selected = 0;
678                                state.selected_file = None;
679                                state.dirty = true;
680                            } else {
681                                state.selected_file = Some(entry.path);
682                                file_selected = true;
683                            }
684                        }
685                        consumed_indices.push(i);
686                    }
687                    KeyCode::Backspace => {
688                        if let Some(parent) = state.current_dir.parent().map(|p| p.to_path_buf()) {
689                            state.current_dir = parent;
690                            state.selected = 0;
691                            state.selected_file = None;
692                            state.dirty = true;
693                        }
694                        consumed_indices.push(i);
695                    }
696                    KeyCode::Char('h') => {
697                        state.show_hidden = !state.show_hidden;
698                        state.selected = 0;
699                        state.dirty = true;
700                        consumed_indices.push(i);
701                    }
702                    KeyCode::Esc => {
703                        state.selected_file = None;
704                        consumed_indices.push(i);
705                    }
706                    _ => {}
707                }
708            }
709            self.consume_indices(consumed_indices);
710        }
711
712        if state.dirty {
713            state.refresh();
714        }
715
716        self.commands
717            .push(Command::BeginContainer(Box::new(BeginContainerArgs {
718                direction: Direction::Column,
719                gap: 0,
720                align: Align::Start,
721                align_self: None,
722                justify: Justify::Start,
723                border: None,
724                border_sides: BorderSides::all(),
725                border_style: Style::new().fg(self.theme.border),
726                bg_color: None,
727                padding: Padding::default(),
728                margin: Margin::default(),
729                constraints: Constraints::default(),
730                title: None,
731                grow: 0,
732                group_name: None,
733            })));
734
735        let dir_text = {
736            let dir = state.current_dir.display().to_string();
737            let mut text = String::with_capacity(5 + dir.len());
738            text.push_str("Dir: ");
739            text.push_str(&dir);
740            text
741        };
742        self.styled(dir_text, Style::new().fg(self.theme.text_dim).dim());
743
744        if state.entries.is_empty() {
745            self.styled("(empty)", Style::new().fg(self.theme.text_dim).dim());
746        } else {
747            for (idx, entry) in state.entries.iter().enumerate() {
748                let icon = if entry.is_dir { "▸ " } else { "  " };
749                let row = if entry.is_dir {
750                    let mut row = String::with_capacity(icon.len() + entry.name.len());
751                    row.push_str(icon);
752                    row.push_str(&entry.name);
753                    row
754                } else {
755                    let size_text = entry.size.to_string();
756                    let mut row =
757                        String::with_capacity(icon.len() + entry.name.len() + size_text.len() + 4);
758                    row.push_str(icon);
759                    row.push_str(&entry.name);
760                    row.push_str("  ");
761                    row.push_str(&size_text);
762                    row.push_str(" B");
763                    row
764                };
765
766                let style = if idx == state.selected {
767                    if focused {
768                        Style::new().bold().fg(self.theme.primary)
769                    } else {
770                        Style::new().fg(self.theme.primary)
771                    }
772                } else {
773                    Style::new().fg(self.theme.text)
774                };
775                self.styled(row, style);
776            }
777        }
778
779        self.commands.push(Command::EndContainer);
780        self.rollback.last_text_idx = None;
781
782        response.changed = file_selected;
783        response
784    }
785}