slt/context/widgets_input/
textarea_progress.rs1use super::*;
2
3impl Context {
4 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(¤t);
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 pub fn progress(&mut self, ratio: f64) -> &mut Self {
291 self.progress_bar(ratio, 20)
292 }
293
294 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 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}