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