Skip to main content

slt/context/widgets_input/
textarea_progress.rs

1use super::*;
2
3impl Context {
4    ///
5    /// When focused, handles character input, Enter (new line), Backspace,
6    /// arrow keys, Home, and End. The cursor is rendered as a block character.
7    ///
8    /// Set [`TextareaState::word_wrap`] to enable soft-wrapping at a given
9    /// display-column width. Up/Down then navigate visual lines.
10    pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> Response {
11        if state.lines.is_empty() {
12            state.lines.push(String::new());
13        }
14        let old_lines = state.lines.clone();
15        state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
16        state.cursor_col = state
17            .cursor_col
18            .min(state.lines[state.cursor_row].chars().count());
19
20        let focused = self.register_focusable();
21        let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
22        let wrapping = state.wrap_width.is_some();
23
24        let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
25
26        if focused {
27            let mut consumed_indices = Vec::new();
28            for (i, event) in self.events.iter().enumerate() {
29                if let Event::Key(key) = event {
30                    if key.kind != KeyEventKind::Press {
31                        continue;
32                    }
33                    match key.code {
34                        KeyCode::Char(ch) => {
35                            if let Some(max) = state.max_length {
36                                let total: usize =
37                                    state.lines.iter().map(|line| line.chars().count()).sum();
38                                if total >= max {
39                                    continue;
40                                }
41                            }
42                            let index = byte_index_for_char(
43                                &state.lines[state.cursor_row],
44                                state.cursor_col,
45                            );
46                            state.lines[state.cursor_row].insert(index, ch);
47                            state.cursor_col += 1;
48                            consumed_indices.push(i);
49                        }
50                        KeyCode::Enter => {
51                            let split_index = byte_index_for_char(
52                                &state.lines[state.cursor_row],
53                                state.cursor_col,
54                            );
55                            let remainder = state.lines[state.cursor_row].split_off(split_index);
56                            state.cursor_row += 1;
57                            state.lines.insert(state.cursor_row, remainder);
58                            state.cursor_col = 0;
59                            consumed_indices.push(i);
60                        }
61                        KeyCode::Backspace => {
62                            if state.cursor_col > 0 {
63                                let start = byte_index_for_char(
64                                    &state.lines[state.cursor_row],
65                                    state.cursor_col - 1,
66                                );
67                                let end = byte_index_for_char(
68                                    &state.lines[state.cursor_row],
69                                    state.cursor_col,
70                                );
71                                state.lines[state.cursor_row].replace_range(start..end, "");
72                                state.cursor_col -= 1;
73                            } else if state.cursor_row > 0 {
74                                let current = state.lines.remove(state.cursor_row);
75                                state.cursor_row -= 1;
76                                state.cursor_col = state.lines[state.cursor_row].chars().count();
77                                state.lines[state.cursor_row].push_str(&current);
78                            }
79                            consumed_indices.push(i);
80                        }
81                        KeyCode::Left => {
82                            if state.cursor_col > 0 {
83                                state.cursor_col -= 1;
84                            } else if state.cursor_row > 0 {
85                                state.cursor_row -= 1;
86                                state.cursor_col = state.lines[state.cursor_row].chars().count();
87                            }
88                            consumed_indices.push(i);
89                        }
90                        KeyCode::Right => {
91                            let line_len = state.lines[state.cursor_row].chars().count();
92                            if state.cursor_col < line_len {
93                                state.cursor_col += 1;
94                            } else if state.cursor_row + 1 < state.lines.len() {
95                                state.cursor_row += 1;
96                                state.cursor_col = 0;
97                            }
98                            consumed_indices.push(i);
99                        }
100                        KeyCode::Up => {
101                            if wrapping {
102                                let (vrow, vcol) = textarea_logical_to_visual(
103                                    &pre_vlines,
104                                    state.cursor_row,
105                                    state.cursor_col,
106                                );
107                                if vrow > 0 {
108                                    let (lr, lc) =
109                                        textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
110                                    state.cursor_row = lr;
111                                    state.cursor_col = lc;
112                                }
113                            } else if state.cursor_row > 0 {
114                                state.cursor_row -= 1;
115                                state.cursor_col = state
116                                    .cursor_col
117                                    .min(state.lines[state.cursor_row].chars().count());
118                            }
119                            consumed_indices.push(i);
120                        }
121                        KeyCode::Down => {
122                            if wrapping {
123                                let (vrow, vcol) = textarea_logical_to_visual(
124                                    &pre_vlines,
125                                    state.cursor_row,
126                                    state.cursor_col,
127                                );
128                                if vrow + 1 < pre_vlines.len() {
129                                    let (lr, lc) =
130                                        textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
131                                    state.cursor_row = lr;
132                                    state.cursor_col = lc;
133                                }
134                            } else if state.cursor_row + 1 < state.lines.len() {
135                                state.cursor_row += 1;
136                                state.cursor_col = state
137                                    .cursor_col
138                                    .min(state.lines[state.cursor_row].chars().count());
139                            }
140                            consumed_indices.push(i);
141                        }
142                        KeyCode::Home => {
143                            state.cursor_col = 0;
144                            consumed_indices.push(i);
145                        }
146                        KeyCode::Delete => {
147                            let line_len = state.lines[state.cursor_row].chars().count();
148                            if state.cursor_col < line_len {
149                                let start = byte_index_for_char(
150                                    &state.lines[state.cursor_row],
151                                    state.cursor_col,
152                                );
153                                let end = byte_index_for_char(
154                                    &state.lines[state.cursor_row],
155                                    state.cursor_col + 1,
156                                );
157                                state.lines[state.cursor_row].replace_range(start..end, "");
158                            } else if state.cursor_row + 1 < state.lines.len() {
159                                let next = state.lines.remove(state.cursor_row + 1);
160                                state.lines[state.cursor_row].push_str(&next);
161                            }
162                            consumed_indices.push(i);
163                        }
164                        KeyCode::End => {
165                            state.cursor_col = state.lines[state.cursor_row].chars().count();
166                            consumed_indices.push(i);
167                        }
168                        _ => {}
169                    }
170                }
171                if let Event::Paste(ref text) = event {
172                    for ch in text.chars() {
173                        if ch == '\n' || ch == '\r' {
174                            let split_index = byte_index_for_char(
175                                &state.lines[state.cursor_row],
176                                state.cursor_col,
177                            );
178                            let remainder = state.lines[state.cursor_row].split_off(split_index);
179                            state.cursor_row += 1;
180                            state.lines.insert(state.cursor_row, remainder);
181                            state.cursor_col = 0;
182                        } else {
183                            if let Some(max) = state.max_length {
184                                let total: usize =
185                                    state.lines.iter().map(|l| l.chars().count()).sum();
186                                if total >= max {
187                                    break;
188                                }
189                            }
190                            let index = byte_index_for_char(
191                                &state.lines[state.cursor_row],
192                                state.cursor_col,
193                            );
194                            state.lines[state.cursor_row].insert(index, ch);
195                            state.cursor_col += 1;
196                        }
197                    }
198                    consumed_indices.push(i);
199                }
200            }
201
202            for index in consumed_indices {
203                self.consumed[index] = true;
204            }
205        }
206
207        let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
208        let (cursor_vrow, cursor_vcol) =
209            textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
210
211        if cursor_vrow < state.scroll_offset {
212            state.scroll_offset = cursor_vrow;
213        }
214        if cursor_vrow >= state.scroll_offset + visible_rows as usize {
215            state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
216        }
217
218        let interaction_id = self.next_interaction_id();
219        let mut response = self.response_for(interaction_id);
220        response.focused = focused;
221        self.commands.push(Command::BeginContainer {
222            direction: Direction::Column,
223            gap: 0,
224            align: Align::Start,
225            align_self: None,
226            justify: Justify::Start,
227            border: None,
228            border_sides: BorderSides::all(),
229            border_style: Style::new().fg(self.theme.border),
230            bg_color: None,
231            padding: Padding::default(),
232            margin: Margin::default(),
233            constraints: Constraints::default(),
234            title: None,
235            grow: 0,
236            group_name: None,
237        });
238
239        for vi in 0..visible_rows as usize {
240            let actual_vi = state.scroll_offset + vi;
241            let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
242                let line = &state.lines[vl.logical_row];
243                let text: String = line
244                    .chars()
245                    .skip(vl.char_start)
246                    .take(vl.char_count)
247                    .collect();
248                (text, actual_vi == cursor_vrow)
249            } else {
250                (String::new(), false)
251            };
252
253            let mut rendered = seg_text.clone();
254            let mut cursor_offset = None;
255            let mut style = if seg_text.is_empty() {
256                Style::new().fg(self.theme.text_dim)
257            } else {
258                Style::new().fg(self.theme.text)
259            };
260
261            if is_cursor_line && focused {
262                rendered.clear();
263                for (idx, ch) in seg_text.chars().enumerate() {
264                    if idx == cursor_vcol {
265                        cursor_offset = Some(rendered.chars().count());
266                        rendered.push('▎');
267                    }
268                    rendered.push(ch);
269                }
270                if cursor_vcol >= seg_text.chars().count() {
271                    cursor_offset = Some(rendered.chars().count());
272                    rendered.push('▎');
273                }
274                style = Style::new().fg(self.theme.text);
275            }
276
277            self.styled_with_cursor(rendered, style, cursor_offset);
278        }
279        self.commands.push(Command::EndContainer);
280        self.last_text_idx = None;
281
282        response.changed = state.lines != old_lines;
283        response
284    }
285
286    /// Render a progress bar (20 chars wide). `ratio` is clamped to `0.0..=1.0`.
287    ///
288    /// Uses block characters (`█` filled, `░` empty). For a custom width use
289    /// [`Context::progress_bar`].
290    pub fn progress(&mut self, ratio: f64) -> &mut Self {
291        self.progress_bar(ratio, 20)
292    }
293
294    /// Render a progress bar with a custom character width.
295    ///
296    /// `ratio` is clamped to `0.0..=1.0`. `width` is the total number of
297    /// characters rendered.
298    /// Render a progress bar filled to the given ratio (0.0–1.0).
299    pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
300        self.progress_bar_colored(ratio, width, self.theme.primary)
301    }
302
303    /// Render a progress bar with a custom fill color.
304    pub fn progress_bar_colored(&mut self, ratio: f64, width: u32, color: Color) -> &mut Self {
305        let clamped = ratio.clamp(0.0, 1.0);
306        let filled = (clamped * width as f64).round() as u32;
307        let empty = width.saturating_sub(filled);
308        let mut bar = String::new();
309        for _ in 0..filled {
310            bar.push('█');
311        }
312        for _ in 0..empty {
313            bar.push('░');
314        }
315        self.styled(bar, Style::new().fg(color))
316    }
317}