bottom/canvas/widgets/
process_table.rs

1use tui::{
2    Frame,
3    layout::{Alignment, Constraint, Direction, Layout, Rect},
4    style::Style,
5    text::{Line, Span},
6    widgets::Paragraph,
7};
8use unicode_segmentation::UnicodeSegmentation;
9
10use crate::{
11    app::{App, AppSearchState},
12    canvas::{
13        Painter,
14        components::data_table::{DrawInfo, SelectionState},
15        drawing_utils::widget_block,
16    },
17};
18
19const SORT_MENU_WIDTH: u16 = 7;
20
21impl Painter {
22    /// Draws and handles all process-related drawing.  Use this.
23    /// - `widget_id` here represents the widget ID of the process widget
24    ///   itself!
25    pub fn draw_process(
26        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
27    ) {
28        if let Some(proc_widget_state) = app_state.states.proc_state.widget_states.get(&widget_id) {
29            let is_basic = app_state.app_config_fields.use_basic_mode;
30            let search_height = if !is_basic { 5 } else { 3 };
31            let is_sort_open = proc_widget_state.is_sort_open;
32
33            let mut proc_draw_loc = draw_loc;
34            if proc_widget_state.is_search_enabled() {
35                let processes_chunk = Layout::default()
36                    .direction(Direction::Vertical)
37                    .constraints([Constraint::Min(0), Constraint::Length(search_height)])
38                    .split(draw_loc);
39                proc_draw_loc = processes_chunk[0];
40
41                self.draw_search_field(f, app_state, processes_chunk[1], widget_id + 1);
42            }
43
44            if is_sort_open {
45                let processes_chunk = Layout::default()
46                    .direction(Direction::Horizontal)
47                    .constraints([Constraint::Length(SORT_MENU_WIDTH + 4), Constraint::Min(0)])
48                    .split(proc_draw_loc);
49                proc_draw_loc = processes_chunk[1];
50
51                self.draw_sort_table(f, app_state, processes_chunk[0], widget_id + 2);
52            }
53
54            self.draw_processes_table(f, app_state, proc_draw_loc, widget_id);
55        }
56
57        if let Some(proc_widget_state) = app_state
58            .states
59            .proc_state
60            .widget_states
61            .get_mut(&widget_id)
62        {
63            // Reset redraw marker.
64            if proc_widget_state.force_rerender {
65                proc_widget_state.force_rerender = false;
66            }
67        }
68    }
69
70    /// Draws the process sort box.
71    /// - `widget_id` represents the widget ID of the process widget itself.
72    fn draw_processes_table(
73        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
74    ) {
75        let should_get_widget_bounds = app_state.should_get_widget_bounds();
76        if let Some(proc_widget_state) = app_state
77            .states
78            .proc_state
79            .widget_states
80            .get_mut(&widget_id)
81        {
82            let recalculate_column_widths =
83                should_get_widget_bounds || proc_widget_state.force_rerender;
84
85            let is_on_widget = widget_id == app_state.current_widget.widget_id;
86
87            let draw_info = DrawInfo {
88                loc: draw_loc,
89                force_redraw: app_state.is_force_redraw,
90                recalculate_column_widths,
91                selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),
92            };
93
94            proc_widget_state.table.draw(
95                f,
96                &draw_info,
97                app_state.widget_map.get_mut(&widget_id),
98                self,
99            );
100        }
101    }
102
103    /// Draws the process search field.
104    /// - `widget_id` represents the widget ID of the search box itself --- NOT
105    ///   the process widget state that is stored.
106    fn draw_search_field(
107        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
108    ) {
109        fn build_query_span(
110            search_state: &AppSearchState, available_width: usize, is_on_widget: bool,
111            currently_selected_text_style: Style, text_style: Style,
112        ) -> Vec<Span<'_>> {
113            let start_index = search_state.display_start_char_index;
114            let cursor_index = search_state.grapheme_cursor.cur_cursor();
115            let mut current_width = 0;
116            let query = search_state.current_search_query.as_str();
117
118            if is_on_widget {
119                let mut res = Vec::with_capacity(available_width);
120                for ((index, grapheme), lengths) in
121                    UnicodeSegmentation::grapheme_indices(query, true)
122                        .zip(search_state.size_mappings.values())
123                {
124                    if index < start_index {
125                        continue;
126                    } else if current_width > available_width {
127                        break;
128                    } else {
129                        let styled = if index == cursor_index {
130                            Span::styled(grapheme, currently_selected_text_style)
131                        } else {
132                            Span::styled(grapheme, text_style)
133                        };
134
135                        res.push(styled);
136                        current_width += lengths.end - lengths.start;
137                    }
138                }
139
140                if cursor_index == query.len() {
141                    res.push(Span::styled(" ", currently_selected_text_style))
142                }
143
144                res
145            } else {
146                // This is easier - we just need to get a range of graphemes, rather than
147                // dealing with possibly inserting a cursor (as none is shown!)
148
149                vec![Span::styled(query.to_string(), text_style)]
150            }
151        }
152
153        let is_basic = app_state.app_config_fields.use_basic_mode;
154
155        if let Some(proc_widget_state) = app_state
156            .states
157            .proc_state
158            .widget_states
159            .get_mut(&(widget_id - 1))
160        {
161            let is_selected = widget_id == app_state.current_widget.widget_id;
162            let num_columns = usize::from(draw_loc.width);
163            const SEARCH_TITLE: &str = "> ";
164            let offset = 4;
165            let available_width = if num_columns > (offset + 3) {
166                num_columns - offset
167            } else {
168                num_columns
169            };
170
171            proc_widget_state
172                .proc_search
173                .search_state
174                .get_start_position(available_width, app_state.is_force_redraw);
175
176            // TODO: [CURSOR] blinking cursor?
177            let query_with_cursor = build_query_span(
178                &proc_widget_state.proc_search.search_state,
179                available_width,
180                is_selected,
181                self.styles.selected_text_style,
182                self.styles.text_style,
183            );
184
185            let mut search_text = vec![Line::from({
186                let mut search_vec = vec![Span::styled(
187                    SEARCH_TITLE,
188                    if is_selected {
189                        self.styles.table_header_style
190                    } else {
191                        self.styles.text_style
192                    },
193                )];
194                search_vec.extend(query_with_cursor);
195
196                search_vec
197            })];
198
199            // Text options shamelessly stolen from VS Code.
200            let case_style = if !proc_widget_state.proc_search.is_ignoring_case {
201                self.styles.selected_text_style
202            } else {
203                self.styles.text_style
204            };
205
206            let whole_word_style = if proc_widget_state.proc_search.is_searching_whole_word {
207                self.styles.selected_text_style
208            } else {
209                self.styles.text_style
210            };
211
212            let regex_style = if proc_widget_state.proc_search.is_searching_with_regex {
213                self.styles.selected_text_style
214            } else {
215                self.styles.text_style
216            };
217
218            // TODO: [MOUSE] Mouse support for these in search
219            // TODO: [MOVEMENT] Movement support for these in search
220            let (case, whole, regex) = {
221                cfg_if::cfg_if! {
222                    if #[cfg(target_os = "macos")] {
223                        ("Case(F1)", "Whole(F2)", "Regex(F3)")
224                    } else {
225                        ("Case(Alt+C)", "Whole(Alt+W)", "Regex(Alt+R)")
226                    }
227                }
228            };
229            let option_text = Line::from(vec![
230                Span::styled(case, case_style),
231                Span::raw("  "),
232                Span::styled(whole, whole_word_style),
233                Span::raw("  "),
234                Span::styled(regex, regex_style),
235            ]);
236
237            search_text.push(Line::from(Span::styled(
238                if let Some(err) = &proc_widget_state.proc_search.search_state.error_message {
239                    err.as_str()
240                } else {
241                    ""
242                },
243                self.styles.invalid_query_style,
244            )));
245            search_text.push(option_text);
246
247            let current_border_style =
248                if proc_widget_state.proc_search.search_state.is_invalid_search {
249                    self.styles.invalid_query_style
250                } else if is_selected {
251                    self.styles.highlighted_border_style
252                } else {
253                    self.styles.border_style
254                };
255
256            let process_search_block = {
257                let mut block = widget_block(is_basic, is_selected, self.styles.border_type)
258                    .border_style(current_border_style);
259
260                if !is_basic {
261                    block = block.title_top(
262                        Line::styled(" Esc to close ", current_border_style).right_aligned(),
263                    )
264                }
265
266                block
267            };
268
269            let margined_draw_loc = Layout::default()
270                .constraints([Constraint::Percentage(100)])
271                .horizontal_margin(u16::from(is_basic && !is_selected))
272                .direction(Direction::Horizontal)
273                .split(draw_loc)[0];
274
275            f.render_widget(
276                Paragraph::new(search_text)
277                    .block(process_search_block)
278                    .style(self.styles.text_style)
279                    .alignment(Alignment::Left),
280                margined_draw_loc,
281            );
282
283            if app_state.should_get_widget_bounds() {
284                // Update draw loc in widget map
285                if let Some(widget) = app_state.widget_map.get_mut(&widget_id) {
286                    widget.top_left_corner = Some((margined_draw_loc.x, margined_draw_loc.y));
287                    widget.bottom_right_corner = Some((
288                        margined_draw_loc.x + margined_draw_loc.width,
289                        margined_draw_loc.y + margined_draw_loc.height,
290                    ));
291                }
292            }
293        }
294    }
295
296    /// Draws the process sort box.
297    /// - `widget_id` represents the widget ID of the sort box itself --- NOT
298    ///   the process widget state that is stored.
299    fn draw_sort_table(
300        &self, f: &mut Frame<'_>, app_state: &mut App, draw_loc: Rect, widget_id: u64,
301    ) {
302        let should_get_widget_bounds = app_state.should_get_widget_bounds();
303        if let Some(pws) = app_state
304            .states
305            .proc_state
306            .widget_states
307            .get_mut(&(widget_id - 2))
308        {
309            let recalculate_column_widths = should_get_widget_bounds || pws.force_rerender;
310
311            let is_on_widget = widget_id == app_state.current_widget.widget_id;
312
313            let draw_info = DrawInfo {
314                loc: draw_loc,
315                force_redraw: app_state.is_force_redraw,
316                recalculate_column_widths,
317                selection_state: SelectionState::new(app_state.is_expanded, is_on_widget),
318            };
319
320            pws.sort_table.draw(
321                f,
322                &draw_info,
323                app_state.widget_map.get_mut(&widget_id),
324                self,
325            );
326        }
327    }
328}