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, key) in self.available_key_presses() {
29 match key.code {
30 KeyCode::Char(ch) => {
31 if let Some(max) = state.max_length {
32 let total: usize =
33 state.lines.iter().map(|line| line.chars().count()).sum();
34 if total >= max {
35 continue;
36 }
37 }
38 let index =
39 byte_index_for_char(&state.lines[state.cursor_row], state.cursor_col);
40 state.lines[state.cursor_row].insert(index, ch);
41 state.cursor_col += 1;
42 consumed_indices.push(i);
43 }
44 KeyCode::Enter => {
45 let split_index =
46 byte_index_for_char(&state.lines[state.cursor_row], state.cursor_col);
47 let remainder = state.lines[state.cursor_row].split_off(split_index);
48 state.cursor_row += 1;
49 state.lines.insert(state.cursor_row, remainder);
50 state.cursor_col = 0;
51 consumed_indices.push(i);
52 }
53 KeyCode::Backspace => {
54 if state.cursor_col > 0 {
55 let start = byte_index_for_char(
56 &state.lines[state.cursor_row],
57 state.cursor_col - 1,
58 );
59 let end = byte_index_for_char(
60 &state.lines[state.cursor_row],
61 state.cursor_col,
62 );
63 state.lines[state.cursor_row].replace_range(start..end, "");
64 state.cursor_col -= 1;
65 } else if state.cursor_row > 0 {
66 let current = state.lines.remove(state.cursor_row);
67 state.cursor_row -= 1;
68 state.cursor_col = state.lines[state.cursor_row].chars().count();
69 state.lines[state.cursor_row].push_str(¤t);
70 }
71 consumed_indices.push(i);
72 }
73 KeyCode::Left => {
74 if state.cursor_col > 0 {
75 state.cursor_col -= 1;
76 } else if state.cursor_row > 0 {
77 state.cursor_row -= 1;
78 state.cursor_col = state.lines[state.cursor_row].chars().count();
79 }
80 consumed_indices.push(i);
81 }
82 KeyCode::Right => {
83 let line_len = state.lines[state.cursor_row].chars().count();
84 if state.cursor_col < line_len {
85 state.cursor_col += 1;
86 } else if state.cursor_row + 1 < state.lines.len() {
87 state.cursor_row += 1;
88 state.cursor_col = 0;
89 }
90 consumed_indices.push(i);
91 }
92 KeyCode::Up => {
93 if wrapping {
94 let (vrow, vcol) = textarea_logical_to_visual(
95 &pre_vlines,
96 state.cursor_row,
97 state.cursor_col,
98 );
99 if vrow > 0 {
100 let (lr, lc) =
101 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
102 state.cursor_row = lr;
103 state.cursor_col = lc;
104 }
105 } else if state.cursor_row > 0 {
106 state.cursor_row -= 1;
107 state.cursor_col = state
108 .cursor_col
109 .min(state.lines[state.cursor_row].chars().count());
110 }
111 consumed_indices.push(i);
112 }
113 KeyCode::Down => {
114 if wrapping {
115 let (vrow, vcol) = textarea_logical_to_visual(
116 &pre_vlines,
117 state.cursor_row,
118 state.cursor_col,
119 );
120 if vrow + 1 < pre_vlines.len() {
121 let (lr, lc) =
122 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
123 state.cursor_row = lr;
124 state.cursor_col = lc;
125 }
126 } else if state.cursor_row + 1 < state.lines.len() {
127 state.cursor_row += 1;
128 state.cursor_col = state
129 .cursor_col
130 .min(state.lines[state.cursor_row].chars().count());
131 }
132 consumed_indices.push(i);
133 }
134 KeyCode::Home => {
135 state.cursor_col = 0;
136 consumed_indices.push(i);
137 }
138 KeyCode::Delete => {
139 let line_len = state.lines[state.cursor_row].chars().count();
140 if state.cursor_col < line_len {
141 let start = byte_index_for_char(
142 &state.lines[state.cursor_row],
143 state.cursor_col,
144 );
145 let end = byte_index_for_char(
146 &state.lines[state.cursor_row],
147 state.cursor_col + 1,
148 );
149 state.lines[state.cursor_row].replace_range(start..end, "");
150 } else if state.cursor_row + 1 < state.lines.len() {
151 let next = state.lines.remove(state.cursor_row + 1);
152 state.lines[state.cursor_row].push_str(&next);
153 }
154 consumed_indices.push(i);
155 }
156 KeyCode::End => {
157 state.cursor_col = state.lines[state.cursor_row].chars().count();
158 consumed_indices.push(i);
159 }
160 _ => {}
161 }
162 }
163 for (i, text) in self.available_pastes() {
164 for ch in text.chars() {
165 if ch == '\n' || ch == '\r' {
166 let split_index =
167 byte_index_for_char(&state.lines[state.cursor_row], state.cursor_col);
168 let remainder = state.lines[state.cursor_row].split_off(split_index);
169 state.cursor_row += 1;
170 state.lines.insert(state.cursor_row, remainder);
171 state.cursor_col = 0;
172 } else {
173 if let Some(max) = state.max_length {
174 let total: usize = state.lines.iter().map(|l| l.chars().count()).sum();
175 if total >= max {
176 break;
177 }
178 }
179 let index =
180 byte_index_for_char(&state.lines[state.cursor_row], state.cursor_col);
181 state.lines[state.cursor_row].insert(index, ch);
182 state.cursor_col += 1;
183 }
184 }
185 consumed_indices.push(i);
186 }
187
188 self.consume_indices(consumed_indices);
189 }
190
191 let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
192 let (cursor_vrow, cursor_vcol) =
193 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
194
195 if cursor_vrow < state.scroll_offset {
196 state.scroll_offset = cursor_vrow;
197 }
198 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
199 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
200 }
201
202 let (_interaction_id, mut response) = self.begin_widget_interaction(focused);
203 self.commands.push(Command::BeginContainer {
204 direction: Direction::Column,
205 gap: 0,
206 align: Align::Start,
207 align_self: None,
208 justify: Justify::Start,
209 border: None,
210 border_sides: BorderSides::all(),
211 border_style: Style::new().fg(self.theme.border),
212 bg_color: None,
213 padding: Padding::default(),
214 margin: Margin::default(),
215 constraints: Constraints::default(),
216 title: None,
217 grow: 0,
218 group_name: None,
219 });
220
221 for vi in 0..visible_rows as usize {
222 let actual_vi = state.scroll_offset + vi;
223 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
224 let line = &state.lines[vl.logical_row];
225 let text: String = line
226 .chars()
227 .skip(vl.char_start)
228 .take(vl.char_count)
229 .collect();
230 (text, actual_vi == cursor_vrow)
231 } else {
232 (String::new(), false)
233 };
234
235 let mut rendered = seg_text.clone();
236 let mut cursor_offset = None;
237 let mut style = if seg_text.is_empty() {
238 Style::new().fg(self.theme.text_dim)
239 } else {
240 Style::new().fg(self.theme.text)
241 };
242
243 if is_cursor_line && focused {
244 rendered.clear();
245 for (idx, ch) in seg_text.chars().enumerate() {
246 if idx == cursor_vcol {
247 cursor_offset = Some(rendered.chars().count());
248 rendered.push('▎');
249 }
250 rendered.push(ch);
251 }
252 if cursor_vcol >= seg_text.chars().count() {
253 cursor_offset = Some(rendered.chars().count());
254 rendered.push('▎');
255 }
256 style = Style::new().fg(self.theme.text);
257 }
258
259 self.styled_with_cursor(rendered, style, cursor_offset);
260 }
261 self.commands.push(Command::EndContainer);
262 self.rollback.last_text_idx = None;
263
264 response.changed = state.lines != old_lines;
265 response
266 }
267
268 pub fn progress(&mut self, ratio: f64) -> &mut Self {
273 self.progress_bar(ratio, 20)
274 }
275
276 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
282 self.progress_bar_colored(ratio, width, self.theme.primary)
283 }
284
285 pub fn progress_bar_colored(&mut self, ratio: f64, width: u32, color: Color) -> &mut Self {
287 let clamped = ratio.clamp(0.0, 1.0);
288 let filled = (clamped * width as f64).round() as u32;
289 let empty = width.saturating_sub(filled);
290 let mut bar = String::new();
291 for _ in 0..filled {
292 bar.push('█');
293 }
294 for _ in 0..empty {
295 bar.push('░');
296 }
297 self.styled(bar, Style::new().fg(color))
298 }
299}