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) if !has_global_shortcut_modifier(key.modifiers) => {
128 if let Some(max) = state.max_length
129 && state.grapheme_len() >= max
130 {
131 continue;
132 }
133 if !state.last_was_char_insert {
136 state.push_history();
137 }
138 let index = byte_index_for_grapheme(
139 &state.lines[state.cursor_row],
140 state.cursor_col,
141 );
142 state.lines[state.cursor_row].insert(index, ch);
143 state.cursor_col += 1;
144 state.last_was_char_insert = true;
145 consumed_indices.push(i);
146 }
147 KeyCode::Enter => {
148 if state
149 .max_length
150 .is_some_and(|max| state.grapheme_len() >= max)
151 {
152 continue;
153 }
154 state.push_history();
155 let split_index = byte_index_for_grapheme(
156 &state.lines[state.cursor_row],
157 state.cursor_col,
158 );
159 let remainder = state.lines[state.cursor_row].split_off(split_index);
160 state.cursor_row += 1;
161 state.lines.insert(state.cursor_row, remainder);
162 state.cursor_col = 0;
163 state.last_was_char_insert = false;
164 consumed_indices.push(i);
165 }
166 KeyCode::Backspace => {
167 if state.cursor_col > 0 || state.cursor_row > 0 {
168 state.push_history();
169 }
170 if state.cursor_col > 0 {
171 let start = byte_index_for_grapheme(
172 &state.lines[state.cursor_row],
173 state.cursor_col - 1,
174 );
175 let end = byte_index_for_grapheme(
176 &state.lines[state.cursor_row],
177 state.cursor_col,
178 );
179 state.lines[state.cursor_row].replace_range(start..end, "");
180 state.cursor_col -= 1;
181 } else if state.cursor_row > 0 {
182 let current = state.lines.remove(state.cursor_row);
183 state.cursor_row -= 1;
184 state.cursor_col = grapheme_count(&state.lines[state.cursor_row]);
185 state.lines[state.cursor_row].push_str(¤t);
186 }
187 state.last_was_char_insert = false;
188 consumed_indices.push(i);
189 }
190 KeyCode::Left => {
191 if state.cursor_col > 0 {
192 state.cursor_col -= 1;
193 } else if state.cursor_row > 0 {
194 state.cursor_row -= 1;
195 state.cursor_col = grapheme_count(&state.lines[state.cursor_row]);
196 }
197 state.last_was_char_insert = false;
198 consumed_indices.push(i);
199 }
200 KeyCode::Right => {
201 let line_len = grapheme_count(&state.lines[state.cursor_row]);
202 if state.cursor_col < line_len {
203 state.cursor_col += 1;
204 } else if state.cursor_row + 1 < state.lines.len() {
205 state.cursor_row += 1;
206 state.cursor_col = 0;
207 }
208 state.last_was_char_insert = false;
209 consumed_indices.push(i);
210 }
211 KeyCode::Up => {
212 if wrapping {
213 let (vrow, vcol) = textarea_logical_to_visual(
214 &pre_vlines,
215 state.cursor_row,
216 state.cursor_col,
217 );
218 if vrow > 0 {
219 let (lr, lc) =
220 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
221 state.cursor_row = lr;
222 state.cursor_col = lc;
223 }
224 } else if state.cursor_row > 0 {
225 state.cursor_row -= 1;
226 state.cursor_col = state
227 .cursor_col
228 .min(grapheme_count(&state.lines[state.cursor_row]));
229 }
230 state.last_was_char_insert = false;
231 consumed_indices.push(i);
232 }
233 KeyCode::Down => {
234 if wrapping {
235 let (vrow, vcol) = textarea_logical_to_visual(
236 &pre_vlines,
237 state.cursor_row,
238 state.cursor_col,
239 );
240 if vrow + 1 < pre_vlines.len() {
241 let (lr, lc) =
242 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
243 state.cursor_row = lr;
244 state.cursor_col = lc;
245 }
246 } else if state.cursor_row + 1 < state.lines.len() {
247 state.cursor_row += 1;
248 state.cursor_col = state
249 .cursor_col
250 .min(grapheme_count(&state.lines[state.cursor_row]));
251 }
252 state.last_was_char_insert = false;
253 consumed_indices.push(i);
254 }
255 KeyCode::Home => {
256 state.cursor_col = 0;
257 state.last_was_char_insert = false;
258 consumed_indices.push(i);
259 }
260 KeyCode::Delete => {
261 let line_len = grapheme_count(&state.lines[state.cursor_row]);
262 let will_mutate =
263 state.cursor_col < line_len || state.cursor_row + 1 < state.lines.len();
264 if will_mutate {
265 state.push_history();
266 }
267 if state.cursor_col < line_len {
268 let start = byte_index_for_grapheme(
269 &state.lines[state.cursor_row],
270 state.cursor_col,
271 );
272 let end = byte_index_for_grapheme(
273 &state.lines[state.cursor_row],
274 state.cursor_col + 1,
275 );
276 state.lines[state.cursor_row].replace_range(start..end, "");
277 } else if state.cursor_row + 1 < state.lines.len() {
278 let next = state.lines.remove(state.cursor_row + 1);
279 state.lines[state.cursor_row].push_str(&next);
280 }
281 state.last_was_char_insert = false;
282 consumed_indices.push(i);
283 }
284 KeyCode::End => {
285 state.cursor_col = grapheme_count(&state.lines[state.cursor_row]);
286 state.last_was_char_insert = false;
287 consumed_indices.push(i);
288 }
289 _ => {}
290 }
291 }
292 for (i, text) in self.available_pastes() {
293 let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
294 if !normalized.is_empty() {
297 state.push_history();
298 }
299 let mut total_chars = state.grapheme_len();
300 for cluster in normalized.graphemes(true) {
301 if let Some(max) = state.max_length
302 && total_chars >= max
303 {
304 break;
305 }
306 if cluster == "\n" {
307 let split_index = byte_index_for_grapheme(
308 &state.lines[state.cursor_row],
309 state.cursor_col,
310 );
311 let remainder = state.lines[state.cursor_row].split_off(split_index);
312 state.cursor_row += 1;
313 state.lines.insert(state.cursor_row, remainder);
314 state.cursor_col = 0;
315 total_chars += 1;
316 } else {
317 let before = grapheme_count(&state.lines[state.cursor_row]);
318 let index = byte_index_for_grapheme(
319 &state.lines[state.cursor_row],
320 state.cursor_col,
321 );
322 let inserted_end = index + cluster.len();
323 state.lines[state.cursor_row].insert_str(index, cluster);
324 state.cursor_col =
325 grapheme_count(&state.lines[state.cursor_row][..inserted_end]);
326 let after = grapheme_count(&state.lines[state.cursor_row]);
327 total_chars = total_chars.saturating_sub(before).saturating_add(after);
328 }
329 }
330 state.last_was_char_insert = false;
331 consumed_indices.push(i);
332 }
333
334 self.consume_indices(consumed_indices);
335 }
336
337 let vlines = if state.lines == pre_lines {
338 pre_vlines
339 } else {
340 textarea_build_visual_lines(&state.lines, wrap_w)
341 };
342 let (cursor_vrow, cursor_vcol) =
343 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
344
345 if cursor_vrow < state.scroll_offset {
346 state.scroll_offset = cursor_vrow;
347 }
348 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
349 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
350 }
351
352 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
353 self.commands
354 .push(Command::BeginContainer(Box::new(BeginContainerArgs {
355 direction: Direction::Column,
356 gap: 0,
357 align: Align::Start,
358 align_self: None,
359 justify: Justify::Start,
360 border: None,
361 border_sides: BorderSides::all(),
362 border_style: Style::new().fg(self.theme.border),
363 bg_color: None,
364 padding: Padding::default(),
365 margin: Margin::default(),
366 constraints: Constraints::default(),
367 title: None,
368 grow: 0,
369 group_name: None,
370 })));
371
372 for vi in 0..visible_rows as usize {
373 let actual_vi = state.scroll_offset + vi;
374 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
375 let line = &state.lines[vl.logical_row];
376 let text: String = line
379 .graphemes(true)
380 .skip(vl.char_start)
381 .take(vl.char_count)
382 .collect();
383 (text, actual_vi == cursor_vrow)
384 } else {
385 (String::new(), false)
386 };
387
388 let mut rendered = seg_text.clone();
389 let mut cursor_offset = None;
390 let mut style = if seg_text.is_empty() {
391 Style::new().fg(self.theme.text_dim)
392 } else {
393 Style::new().fg(self.theme.text)
394 };
395
396 if is_cursor_line && focused {
397 rendered.clear();
398 for (idx, g) in seg_text.graphemes(true).enumerate() {
403 if idx == cursor_vcol {
404 cursor_offset = Some(rendered.chars().count());
405 rendered.push('▎');
406 }
407 rendered.push_str(g);
408 }
409 if cursor_vcol >= grapheme_count(&seg_text) {
410 cursor_offset = Some(rendered.chars().count());
411 rendered.push('▎');
412 }
413 style = Style::new().fg(self.theme.text);
414 }
415
416 self.styled_with_cursor(rendered, style, cursor_offset);
417 }
418 self.commands.push(Command::EndContainer);
419 self.rollback.last_text_idx = None;
420
421 response.changed = state.lines != pre_lines;
422 response
423 }
424
425 pub fn progress(&mut self, ratio: f64) -> Response {
435 self.progress_bar(ratio, 20)
436 }
437
438 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> Response {
443 self.progress_bar_colored(ratio, width, self.theme.primary)
444 }
445
446 pub fn progress_bar_colored(&mut self, ratio: f64, width: u32, color: Color) -> Response {
448 let response = self.interaction();
449 let clamped = ratio.clamp(0.0, 1.0);
450 let filled = (clamped * width as f64).round() as u32;
451 let empty = width.saturating_sub(filled);
452 let mut bar = String::with_capacity(width as usize * 3);
453 for _ in 0..filled {
454 bar.push('█');
455 }
456 for _ in 0..empty {
457 bar.push('░');
458 }
459 self.styled(bar, Style::new().fg(color));
460 response
461 }
462}