slt/context/widgets_input/
textarea_progress.rs1use super::*;
2
3fn cluster_is_alphanumeric(cluster: &str) -> bool {
7 cluster.chars().next().is_some_and(|c| c.is_alphanumeric())
8}
9
10fn prev_word_col(line: &str, col: usize) -> usize {
16 let clusters: Vec<&str> = line.graphemes(true).collect();
17 let mut pos = col.min(clusters.len());
18 while pos > 0 && !cluster_is_alphanumeric(clusters[pos - 1]) {
19 pos -= 1;
20 }
21 while pos > 0 && cluster_is_alphanumeric(clusters[pos - 1]) {
22 pos -= 1;
23 }
24 pos
25}
26
27fn next_word_col(line: &str, col: usize) -> usize {
31 let clusters: Vec<&str> = line.graphemes(true).collect();
32 let mut pos = col.min(clusters.len());
33 while pos < clusters.len() && !cluster_is_alphanumeric(clusters[pos]) {
34 pos += 1;
35 }
36 while pos < clusters.len() && cluster_is_alphanumeric(clusters[pos]) {
37 pos += 1;
38 }
39 pos
40}
41
42impl Context {
43 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> Response {
56 if state.lines.is_empty() {
57 state.lines.push(String::new());
58 }
59 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
60 state.cursor_col = state
61 .cursor_col
62 .min(grapheme_count(&state.lines[state.cursor_row]));
63
64 let focused = self.register_focusable();
65 let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
66 let wrapping = state.wrap_width.is_some();
67
68 let pre_lines = state.lines.clone();
69 let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
70
71 if focused {
72 let mut consumed_indices = Vec::new();
73 for (i, key) in self.available_key_presses() {
74 match key.code {
75 KeyCode::Char('z') if key.modifiers.contains(KeyModifiers::CONTROL) => {
76 state.undo();
77 state.last_was_char_insert = false;
78 consumed_indices.push(i);
79 }
80 KeyCode::Char('y') if key.modifiers.contains(KeyModifiers::CONTROL) => {
81 state.redo();
82 state.last_was_char_insert = false;
83 consumed_indices.push(i);
84 }
85 KeyCode::Char('k') if key.modifiers.contains(KeyModifiers::CONTROL) => {
86 let line_len = grapheme_count(&state.lines[state.cursor_row]);
87 if state.cursor_col < line_len {
88 state.push_history();
89 let cut = byte_index_for_grapheme(
90 &state.lines[state.cursor_row],
91 state.cursor_col,
92 );
93 state.lines[state.cursor_row].truncate(cut);
94 }
95 state.last_was_char_insert = false;
96 consumed_indices.push(i);
97 }
98 KeyCode::Left
99 if key.modifiers.contains(KeyModifiers::CONTROL)
100 || key.modifiers.contains(KeyModifiers::ALT) =>
101 {
102 if state.cursor_col > 0 {
103 state.cursor_col =
104 prev_word_col(&state.lines[state.cursor_row], state.cursor_col);
105 } else if state.cursor_row > 0 {
106 state.cursor_row -= 1;
107 state.cursor_col = grapheme_count(&state.lines[state.cursor_row]);
108 }
109 state.last_was_char_insert = false;
110 consumed_indices.push(i);
111 }
112 KeyCode::Right
113 if key.modifiers.contains(KeyModifiers::CONTROL)
114 || key.modifiers.contains(KeyModifiers::ALT) =>
115 {
116 let line_len = grapheme_count(&state.lines[state.cursor_row]);
117 if state.cursor_col < line_len {
118 state.cursor_col =
119 next_word_col(&state.lines[state.cursor_row], state.cursor_col);
120 } else if state.cursor_row + 1 < state.lines.len() {
121 state.cursor_row += 1;
122 state.cursor_col = 0;
123 }
124 state.last_was_char_insert = false;
125 consumed_indices.push(i);
126 }
127 KeyCode::Char(ch) => {
128 if let Some(max) = state.max_length {
129 let total: usize =
130 state.lines.iter().map(|line| grapheme_count(line)).sum();
131 if total >= max {
132 continue;
133 }
134 }
135 if !state.last_was_char_insert {
138 state.push_history();
139 }
140 let index = byte_index_for_grapheme(
141 &state.lines[state.cursor_row],
142 state.cursor_col,
143 );
144 state.lines[state.cursor_row].insert(index, ch);
145 state.cursor_col += 1;
146 state.last_was_char_insert = true;
147 consumed_indices.push(i);
148 }
149 KeyCode::Enter => {
150 state.push_history();
151 let split_index = byte_index_for_grapheme(
152 &state.lines[state.cursor_row],
153 state.cursor_col,
154 );
155 let remainder = state.lines[state.cursor_row].split_off(split_index);
156 state.cursor_row += 1;
157 state.lines.insert(state.cursor_row, remainder);
158 state.cursor_col = 0;
159 state.last_was_char_insert = false;
160 consumed_indices.push(i);
161 }
162 KeyCode::Backspace => {
163 if state.cursor_col > 0 || state.cursor_row > 0 {
164 state.push_history();
165 }
166 if state.cursor_col > 0 {
167 let start = byte_index_for_grapheme(
168 &state.lines[state.cursor_row],
169 state.cursor_col - 1,
170 );
171 let end = byte_index_for_grapheme(
172 &state.lines[state.cursor_row],
173 state.cursor_col,
174 );
175 state.lines[state.cursor_row].replace_range(start..end, "");
176 state.cursor_col -= 1;
177 } else if state.cursor_row > 0 {
178 let current = state.lines.remove(state.cursor_row);
179 state.cursor_row -= 1;
180 state.cursor_col = grapheme_count(&state.lines[state.cursor_row]);
181 state.lines[state.cursor_row].push_str(¤t);
182 }
183 state.last_was_char_insert = false;
184 consumed_indices.push(i);
185 }
186 KeyCode::Left => {
187 if state.cursor_col > 0 {
188 state.cursor_col -= 1;
189 } else if state.cursor_row > 0 {
190 state.cursor_row -= 1;
191 state.cursor_col = grapheme_count(&state.lines[state.cursor_row]);
192 }
193 state.last_was_char_insert = false;
194 consumed_indices.push(i);
195 }
196 KeyCode::Right => {
197 let line_len = grapheme_count(&state.lines[state.cursor_row]);
198 if state.cursor_col < line_len {
199 state.cursor_col += 1;
200 } else if state.cursor_row + 1 < state.lines.len() {
201 state.cursor_row += 1;
202 state.cursor_col = 0;
203 }
204 state.last_was_char_insert = false;
205 consumed_indices.push(i);
206 }
207 KeyCode::Up => {
208 if wrapping {
209 let (vrow, vcol) = textarea_logical_to_visual(
210 &pre_vlines,
211 state.cursor_row,
212 state.cursor_col,
213 );
214 if vrow > 0 {
215 let (lr, lc) =
216 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
217 state.cursor_row = lr;
218 state.cursor_col = lc;
219 }
220 } else if state.cursor_row > 0 {
221 state.cursor_row -= 1;
222 state.cursor_col = state
223 .cursor_col
224 .min(grapheme_count(&state.lines[state.cursor_row]));
225 }
226 state.last_was_char_insert = false;
227 consumed_indices.push(i);
228 }
229 KeyCode::Down => {
230 if wrapping {
231 let (vrow, vcol) = textarea_logical_to_visual(
232 &pre_vlines,
233 state.cursor_row,
234 state.cursor_col,
235 );
236 if vrow + 1 < pre_vlines.len() {
237 let (lr, lc) =
238 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
239 state.cursor_row = lr;
240 state.cursor_col = lc;
241 }
242 } else if state.cursor_row + 1 < state.lines.len() {
243 state.cursor_row += 1;
244 state.cursor_col = state
245 .cursor_col
246 .min(grapheme_count(&state.lines[state.cursor_row]));
247 }
248 state.last_was_char_insert = false;
249 consumed_indices.push(i);
250 }
251 KeyCode::Home => {
252 state.cursor_col = 0;
253 state.last_was_char_insert = false;
254 consumed_indices.push(i);
255 }
256 KeyCode::Delete => {
257 let line_len = grapheme_count(&state.lines[state.cursor_row]);
258 let will_mutate =
259 state.cursor_col < line_len || state.cursor_row + 1 < state.lines.len();
260 if will_mutate {
261 state.push_history();
262 }
263 if state.cursor_col < line_len {
264 let start = byte_index_for_grapheme(
265 &state.lines[state.cursor_row],
266 state.cursor_col,
267 );
268 let end = byte_index_for_grapheme(
269 &state.lines[state.cursor_row],
270 state.cursor_col + 1,
271 );
272 state.lines[state.cursor_row].replace_range(start..end, "");
273 } else if state.cursor_row + 1 < state.lines.len() {
274 let next = state.lines.remove(state.cursor_row + 1);
275 state.lines[state.cursor_row].push_str(&next);
276 }
277 state.last_was_char_insert = false;
278 consumed_indices.push(i);
279 }
280 KeyCode::End => {
281 state.cursor_col = grapheme_count(&state.lines[state.cursor_row]);
282 state.last_was_char_insert = false;
283 consumed_indices.push(i);
284 }
285 _ => {}
286 }
287 }
288 for (i, text) in self.available_pastes() {
289 if !text.is_empty() {
292 state.push_history();
293 }
294 let mut total_chars: usize = state.lines.iter().map(|l| grapheme_count(l)).sum();
298 for ch in text.chars() {
299 if let Some(max) = state.max_length
300 && total_chars >= max
301 {
302 break;
303 }
304 if ch == '\n' || ch == '\r' {
305 let split_index = byte_index_for_grapheme(
306 &state.lines[state.cursor_row],
307 state.cursor_col,
308 );
309 let remainder = state.lines[state.cursor_row].split_off(split_index);
310 state.cursor_row += 1;
311 state.lines.insert(state.cursor_row, remainder);
312 state.cursor_col = 0;
313 total_chars += 1;
314 } else {
315 let index = byte_index_for_grapheme(
316 &state.lines[state.cursor_row],
317 state.cursor_col,
318 );
319 state.lines[state.cursor_row].insert(index, ch);
320 state.cursor_col += 1;
321 total_chars += 1;
322 }
323 }
324 state.last_was_char_insert = false;
325 consumed_indices.push(i);
326 }
327
328 self.consume_indices(consumed_indices);
329 }
330
331 let vlines = if state.lines == pre_lines {
332 pre_vlines
333 } else {
334 textarea_build_visual_lines(&state.lines, wrap_w)
335 };
336 let (cursor_vrow, cursor_vcol) =
337 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
338
339 if cursor_vrow < state.scroll_offset {
340 state.scroll_offset = cursor_vrow;
341 }
342 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
343 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
344 }
345
346 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
347 self.commands
348 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
349 direction: Direction::Column,
350 gap: 0,
351 align: Align::Start,
352 align_self: None,
353 justify: Justify::Start,
354 border: None,
355 border_sides: BorderSides::all(),
356 border_style: Style::new().fg(self.theme.border),
357 bg_color: None,
358 padding: Padding::default(),
359 margin: Margin::default(),
360 constraints: Constraints::default(),
361 title: None,
362 grow: 0,
363 group_name: None,
364 })));
365
366 for vi in 0..visible_rows as usize {
367 let actual_vi = state.scroll_offset + vi;
368 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
369 let line = &state.lines[vl.logical_row];
370 let text: String = line
373 .graphemes(true)
374 .skip(vl.char_start)
375 .take(vl.char_count)
376 .collect();
377 (text, actual_vi == cursor_vrow)
378 } else {
379 (String::new(), false)
380 };
381
382 let mut rendered = seg_text.clone();
383 let mut cursor_offset = None;
384 let mut style = if seg_text.is_empty() {
385 Style::new().fg(self.theme.text_dim)
386 } else {
387 Style::new().fg(self.theme.text)
388 };
389
390 if is_cursor_line && focused {
391 rendered.clear();
392 for (idx, g) in seg_text.graphemes(true).enumerate() {
397 if idx == cursor_vcol {
398 cursor_offset = Some(rendered.chars().count());
399 rendered.push('▎');
400 }
401 rendered.push_str(g);
402 }
403 if cursor_vcol >= grapheme_count(&seg_text) {
404 cursor_offset = Some(rendered.chars().count());
405 rendered.push('▎');
406 }
407 style = Style::new().fg(self.theme.text);
408 }
409
410 self.styled_with_cursor(rendered, style, cursor_offset);
411 }
412 self.commands.push(Command::EndContainer);
413 self.rollback.last_text_idx = None;
414
415 response.changed = state.lines != pre_lines;
416 response
417 }
418
419 pub fn progress(&mut self, ratio: f64) -> Response {
429 self.progress_bar(ratio, 20)
430 }
431
432 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> Response {
437 self.progress_bar_colored(ratio, width, self.theme.primary)
438 }
439
440 pub fn progress_bar_colored(&mut self, ratio: f64, width: u32, color: Color) -> Response {
442 let response = self.interaction();
443 let clamped = ratio.clamp(0.0, 1.0);
444 let filled = (clamped * width as f64).round() as u32;
445 let empty = width.saturating_sub(filled);
446 let mut bar = String::with_capacity(width as usize * 3);
447 for _ in 0..filled {
448 bar.push('█');
449 }
450 for _ in 0..empty {
451 bar.push('░');
452 }
453 self.styled(bar, Style::new().fg(color));
454 response
455 }
456}