1use super::*;
2
3impl Context {
4 pub fn text_input(&mut self, state: &mut TextInputState) -> Response {
20 self.text_input_colored(state, &WidgetColors::new())
21 }
22
23 pub fn text_input_colored(
24 &mut self,
25 state: &mut TextInputState,
26 colors: &WidgetColors,
27 ) -> Response {
28 slt_assert(
29 !state.value.contains('\n'),
30 "text_input got a newline — use textarea instead",
31 );
32 let focused = self.register_focusable();
33 let old_value = state.value.clone();
34 state.cursor = state.cursor.min(state.value.chars().count());
35
36 if focused {
37 let mut consumed_indices = Vec::new();
38 for (i, event) in self.events.iter().enumerate() {
39 if let Event::Key(key) = event {
40 if key.kind != KeyEventKind::Press {
41 continue;
42 }
43 let matched_suggestions = if state.show_suggestions {
44 state
45 .matched_suggestions()
46 .into_iter()
47 .map(str::to_string)
48 .collect::<Vec<String>>()
49 } else {
50 Vec::new()
51 };
52 let suggestions_visible = !matched_suggestions.is_empty();
53 if suggestions_visible {
54 state.suggestion_index = state
55 .suggestion_index
56 .min(matched_suggestions.len().saturating_sub(1));
57 }
58 match key.code {
59 KeyCode::Up if suggestions_visible => {
60 state.suggestion_index = state.suggestion_index.saturating_sub(1);
61 consumed_indices.push(i);
62 }
63 KeyCode::Down if suggestions_visible => {
64 state.suggestion_index = (state.suggestion_index + 1)
65 .min(matched_suggestions.len().saturating_sub(1));
66 consumed_indices.push(i);
67 }
68 KeyCode::Esc if state.show_suggestions => {
69 state.show_suggestions = false;
70 state.suggestion_index = 0;
71 consumed_indices.push(i);
72 }
73 KeyCode::Tab if suggestions_visible => {
74 if let Some(selected) = matched_suggestions
75 .get(state.suggestion_index)
76 .or_else(|| matched_suggestions.first())
77 {
78 state.value = selected.clone();
79 state.cursor = state.value.chars().count();
80 state.show_suggestions = false;
81 state.suggestion_index = 0;
82 }
83 consumed_indices.push(i);
84 }
85 KeyCode::Char(ch) => {
86 if let Some(max) = state.max_length {
87 if state.value.chars().count() >= max {
88 continue;
89 }
90 }
91 let index = byte_index_for_char(&state.value, state.cursor);
92 state.value.insert(index, ch);
93 state.cursor += 1;
94 if !state.suggestions.is_empty() {
95 state.show_suggestions = true;
96 state.suggestion_index = 0;
97 }
98 consumed_indices.push(i);
99 }
100 KeyCode::Backspace => {
101 if state.cursor > 0 {
102 let start = byte_index_for_char(&state.value, state.cursor - 1);
103 let end = byte_index_for_char(&state.value, state.cursor);
104 state.value.replace_range(start..end, "");
105 state.cursor -= 1;
106 }
107 if !state.suggestions.is_empty() {
108 state.show_suggestions = true;
109 state.suggestion_index = 0;
110 }
111 consumed_indices.push(i);
112 }
113 KeyCode::Left => {
114 state.cursor = state.cursor.saturating_sub(1);
115 consumed_indices.push(i);
116 }
117 KeyCode::Right => {
118 state.cursor = (state.cursor + 1).min(state.value.chars().count());
119 consumed_indices.push(i);
120 }
121 KeyCode::Home => {
122 state.cursor = 0;
123 consumed_indices.push(i);
124 }
125 KeyCode::Delete => {
126 let len = state.value.chars().count();
127 if state.cursor < len {
128 let start = byte_index_for_char(&state.value, state.cursor);
129 let end = byte_index_for_char(&state.value, state.cursor + 1);
130 state.value.replace_range(start..end, "");
131 }
132 if !state.suggestions.is_empty() {
133 state.show_suggestions = true;
134 state.suggestion_index = 0;
135 }
136 consumed_indices.push(i);
137 }
138 KeyCode::End => {
139 state.cursor = state.value.chars().count();
140 consumed_indices.push(i);
141 }
142 _ => {}
143 }
144 }
145 if let Event::Paste(ref text) = event {
146 for ch in text.chars() {
147 if let Some(max) = state.max_length {
148 if state.value.chars().count() >= max {
149 break;
150 }
151 }
152 let index = byte_index_for_char(&state.value, state.cursor);
153 state.value.insert(index, ch);
154 state.cursor += 1;
155 }
156 if !state.suggestions.is_empty() {
157 state.show_suggestions = true;
158 state.suggestion_index = 0;
159 }
160 consumed_indices.push(i);
161 }
162 }
163
164 for index in consumed_indices {
165 self.consumed[index] = true;
166 }
167 }
168
169 if state.value.is_empty() {
170 state.show_suggestions = false;
171 state.suggestion_index = 0;
172 }
173
174 let matched_suggestions = if state.show_suggestions {
175 state
176 .matched_suggestions()
177 .into_iter()
178 .map(str::to_string)
179 .collect::<Vec<String>>()
180 } else {
181 Vec::new()
182 };
183 if !matched_suggestions.is_empty() {
184 state.suggestion_index = state
185 .suggestion_index
186 .min(matched_suggestions.len().saturating_sub(1));
187 }
188
189 let visible_width = self.area_width.saturating_sub(4) as usize;
190 let input_text = if state.value.is_empty() {
191 if state.placeholder.len() > 100 {
192 slt_warn(
193 "text_input placeholder is very long (>100 chars) — consider shortening it",
194 );
195 }
196 let mut ph = state.placeholder.clone();
197 if focused {
198 ph.insert(0, '▎');
199 }
200 ph
201 } else {
202 let chars: Vec<char> = state.value.chars().collect();
203 let display_chars: Vec<char> = if state.masked {
204 vec!['•'; chars.len()]
205 } else {
206 chars.clone()
207 };
208
209 let cursor_display_pos: usize = display_chars[..state.cursor.min(display_chars.len())]
210 .iter()
211 .map(|c| UnicodeWidthChar::width(*c).unwrap_or(1))
212 .sum();
213
214 let scroll_offset = if cursor_display_pos >= visible_width {
215 cursor_display_pos - visible_width + 1
216 } else {
217 0
218 };
219
220 let mut rendered = String::new();
221 let mut current_width: usize = 0;
222 for (idx, &ch) in display_chars.iter().enumerate() {
223 let cw = UnicodeWidthChar::width(ch).unwrap_or(1);
224 if current_width + cw <= scroll_offset {
225 current_width += cw;
226 continue;
227 }
228 if current_width - scroll_offset >= visible_width {
229 break;
230 }
231 if focused && idx == state.cursor {
232 rendered.push('▎');
233 }
234 rendered.push(ch);
235 current_width += cw;
236 }
237 if focused && state.cursor >= display_chars.len() {
238 rendered.push('▎');
239 }
240 rendered
241 };
242 let input_style = if state.value.is_empty() && !focused {
243 Style::new()
244 .dim()
245 .fg(colors.fg.unwrap_or(self.theme.text_dim))
246 } else {
247 Style::new().fg(colors.fg.unwrap_or(self.theme.text))
248 };
249
250 let border_color = if focused {
251 colors.accent.unwrap_or(self.theme.primary)
252 } else if state.validation_error.is_some() {
253 colors.accent.unwrap_or(self.theme.error)
254 } else {
255 colors.border.unwrap_or(self.theme.border)
256 };
257
258 let mut response = self
259 .bordered(Border::Rounded)
260 .border_style(Style::new().fg(border_color))
261 .px(1)
262 .col(|ui| {
263 ui.styled(input_text, input_style);
264 });
265 response.focused = focused;
266 response.changed = state.value != old_value;
267
268 let errors = state.errors();
269 if !errors.is_empty() {
270 for error in errors {
271 self.styled(
272 format!("⚠ {error}"),
273 Style::new()
274 .dim()
275 .fg(colors.accent.unwrap_or(self.theme.error)),
276 );
277 }
278 } else if let Some(error) = state.validation_error.clone() {
279 self.styled(
280 format!("⚠ {error}"),
281 Style::new()
282 .dim()
283 .fg(colors.accent.unwrap_or(self.theme.error)),
284 );
285 }
286
287 if state.show_suggestions && !matched_suggestions.is_empty() {
288 let start = state.suggestion_index.saturating_sub(4);
289 let end = (start + 5).min(matched_suggestions.len());
290 let suggestion_border = colors.border.unwrap_or(self.theme.border);
291 self.bordered(Border::Rounded)
292 .border_style(Style::new().fg(suggestion_border))
293 .px(1)
294 .col(|ui| {
295 for (idx, suggestion) in matched_suggestions[start..end].iter().enumerate() {
296 let actual_idx = start + idx;
297 if actual_idx == state.suggestion_index {
298 ui.styled(
299 suggestion.clone(),
300 Style::new()
301 .bg(colors.accent.unwrap_or(ui.theme().selected_bg))
302 .fg(colors.fg.unwrap_or(ui.theme().selected_fg)),
303 );
304 } else {
305 ui.styled(
306 suggestion.clone(),
307 Style::new().fg(colors.fg.unwrap_or(ui.theme().text)),
308 );
309 }
310 }
311 });
312 }
313 response
314 }
315
316 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
322 self.styled(
323 state.frame(self.tick).to_string(),
324 Style::new().fg(self.theme.primary),
325 )
326 }
327
328 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
333 state.cleanup(self.tick);
334 if state.messages.is_empty() {
335 return self;
336 }
337
338 self.interaction_count += 1;
339 self.commands.push(Command::BeginContainer {
340 direction: Direction::Column,
341 gap: 0,
342 align: Align::Start,
343 align_self: None,
344 justify: Justify::Start,
345 border: None,
346 border_sides: BorderSides::all(),
347 border_style: Style::new().fg(self.theme.border),
348 bg_color: None,
349 padding: Padding::default(),
350 margin: Margin::default(),
351 constraints: Constraints::default(),
352 title: None,
353 grow: 0,
354 group_name: None,
355 });
356 for message in state.messages.iter().rev() {
357 let color = match message.level {
358 ToastLevel::Info => self.theme.primary,
359 ToastLevel::Success => self.theme.success,
360 ToastLevel::Warning => self.theme.warning,
361 ToastLevel::Error => self.theme.error,
362 };
363 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
364 }
365 self.commands.push(Command::EndContainer);
366 self.last_text_idx = None;
367
368 self
369 }
370
371 pub fn slider(
383 &mut self,
384 label: &str,
385 value: &mut f64,
386 range: std::ops::RangeInclusive<f64>,
387 ) -> Response {
388 let focused = self.register_focusable();
389 let mut changed = false;
390
391 let start = *range.start();
392 let end = *range.end();
393 let span = (end - start).max(0.0);
394 let step = if span > 0.0 { span / 20.0 } else { 0.0 };
395
396 *value = (*value).clamp(start, end);
397
398 if focused {
399 let mut consumed_indices = Vec::new();
400 for (i, event) in self.events.iter().enumerate() {
401 if let Event::Key(key) = event {
402 if key.kind != KeyEventKind::Press {
403 continue;
404 }
405
406 match key.code {
407 KeyCode::Left | KeyCode::Char('h') => {
408 if step > 0.0 {
409 let next = (*value - step).max(start);
410 if (next - *value).abs() > f64::EPSILON {
411 *value = next;
412 changed = true;
413 }
414 }
415 consumed_indices.push(i);
416 }
417 KeyCode::Right | KeyCode::Char('l') => {
418 if step > 0.0 {
419 let next = (*value + step).min(end);
420 if (next - *value).abs() > f64::EPSILON {
421 *value = next;
422 changed = true;
423 }
424 }
425 consumed_indices.push(i);
426 }
427 _ => {}
428 }
429 }
430 }
431
432 for idx in consumed_indices {
433 self.consumed[idx] = true;
434 }
435 }
436
437 let ratio = if span <= f64::EPSILON {
438 0.0
439 } else {
440 ((*value - start) / span).clamp(0.0, 1.0)
441 };
442
443 let value_text = format_compact_number(*value);
444 let label_width = UnicodeWidthStr::width(label) as u32;
445 let value_width = UnicodeWidthStr::width(value_text.as_str()) as u32;
446 let track_width = self
447 .area_width
448 .saturating_sub(label_width + value_width + 8)
449 .max(10) as usize;
450 let thumb_idx = if track_width <= 1 {
451 0
452 } else {
453 (ratio * (track_width as f64 - 1.0)).round() as usize
454 };
455
456 let mut track = String::with_capacity(track_width);
457 for i in 0..track_width {
458 if i == thumb_idx {
459 track.push('○');
460 } else if i < thumb_idx {
461 track.push('█');
462 } else {
463 track.push('━');
464 }
465 }
466
467 let text_color = self.theme.text;
468 let border_color = self.theme.border;
469 let primary_color = self.theme.primary;
470 let dim_color = self.theme.text_dim;
471 let mut response = self.container().row(|ui| {
472 ui.text(label).fg(text_color);
473 ui.text("[").fg(border_color);
474 ui.text(track).grow(1).fg(primary_color);
475 ui.text("]").fg(border_color);
476 if focused {
477 ui.text(value_text.as_str()).bold().fg(primary_color);
478 } else {
479 ui.text(value_text.as_str()).fg(dim_color);
480 }
481 });
482 response.focused = focused;
483 response.changed = changed;
484 response
485 }
486
487 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> Response {
495 if state.lines.is_empty() {
496 state.lines.push(String::new());
497 }
498 let old_lines = state.lines.clone();
499 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
500 state.cursor_col = state
501 .cursor_col
502 .min(state.lines[state.cursor_row].chars().count());
503
504 let focused = self.register_focusable();
505 let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
506 let wrapping = state.wrap_width.is_some();
507
508 let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
509
510 if focused {
511 let mut consumed_indices = Vec::new();
512 for (i, event) in self.events.iter().enumerate() {
513 if let Event::Key(key) = event {
514 if key.kind != KeyEventKind::Press {
515 continue;
516 }
517 match key.code {
518 KeyCode::Char(ch) => {
519 if let Some(max) = state.max_length {
520 let total: usize =
521 state.lines.iter().map(|line| line.chars().count()).sum();
522 if total >= max {
523 continue;
524 }
525 }
526 let index = byte_index_for_char(
527 &state.lines[state.cursor_row],
528 state.cursor_col,
529 );
530 state.lines[state.cursor_row].insert(index, ch);
531 state.cursor_col += 1;
532 consumed_indices.push(i);
533 }
534 KeyCode::Enter => {
535 let split_index = byte_index_for_char(
536 &state.lines[state.cursor_row],
537 state.cursor_col,
538 );
539 let remainder = state.lines[state.cursor_row].split_off(split_index);
540 state.cursor_row += 1;
541 state.lines.insert(state.cursor_row, remainder);
542 state.cursor_col = 0;
543 consumed_indices.push(i);
544 }
545 KeyCode::Backspace => {
546 if state.cursor_col > 0 {
547 let start = byte_index_for_char(
548 &state.lines[state.cursor_row],
549 state.cursor_col - 1,
550 );
551 let end = byte_index_for_char(
552 &state.lines[state.cursor_row],
553 state.cursor_col,
554 );
555 state.lines[state.cursor_row].replace_range(start..end, "");
556 state.cursor_col -= 1;
557 } else if state.cursor_row > 0 {
558 let current = state.lines.remove(state.cursor_row);
559 state.cursor_row -= 1;
560 state.cursor_col = state.lines[state.cursor_row].chars().count();
561 state.lines[state.cursor_row].push_str(¤t);
562 }
563 consumed_indices.push(i);
564 }
565 KeyCode::Left => {
566 if state.cursor_col > 0 {
567 state.cursor_col -= 1;
568 } else if state.cursor_row > 0 {
569 state.cursor_row -= 1;
570 state.cursor_col = state.lines[state.cursor_row].chars().count();
571 }
572 consumed_indices.push(i);
573 }
574 KeyCode::Right => {
575 let line_len = state.lines[state.cursor_row].chars().count();
576 if state.cursor_col < line_len {
577 state.cursor_col += 1;
578 } else if state.cursor_row + 1 < state.lines.len() {
579 state.cursor_row += 1;
580 state.cursor_col = 0;
581 }
582 consumed_indices.push(i);
583 }
584 KeyCode::Up => {
585 if wrapping {
586 let (vrow, vcol) = textarea_logical_to_visual(
587 &pre_vlines,
588 state.cursor_row,
589 state.cursor_col,
590 );
591 if vrow > 0 {
592 let (lr, lc) =
593 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
594 state.cursor_row = lr;
595 state.cursor_col = lc;
596 }
597 } else if state.cursor_row > 0 {
598 state.cursor_row -= 1;
599 state.cursor_col = state
600 .cursor_col
601 .min(state.lines[state.cursor_row].chars().count());
602 }
603 consumed_indices.push(i);
604 }
605 KeyCode::Down => {
606 if wrapping {
607 let (vrow, vcol) = textarea_logical_to_visual(
608 &pre_vlines,
609 state.cursor_row,
610 state.cursor_col,
611 );
612 if vrow + 1 < pre_vlines.len() {
613 let (lr, lc) =
614 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
615 state.cursor_row = lr;
616 state.cursor_col = lc;
617 }
618 } else if state.cursor_row + 1 < state.lines.len() {
619 state.cursor_row += 1;
620 state.cursor_col = state
621 .cursor_col
622 .min(state.lines[state.cursor_row].chars().count());
623 }
624 consumed_indices.push(i);
625 }
626 KeyCode::Home => {
627 state.cursor_col = 0;
628 consumed_indices.push(i);
629 }
630 KeyCode::Delete => {
631 let line_len = state.lines[state.cursor_row].chars().count();
632 if state.cursor_col < line_len {
633 let start = byte_index_for_char(
634 &state.lines[state.cursor_row],
635 state.cursor_col,
636 );
637 let end = byte_index_for_char(
638 &state.lines[state.cursor_row],
639 state.cursor_col + 1,
640 );
641 state.lines[state.cursor_row].replace_range(start..end, "");
642 } else if state.cursor_row + 1 < state.lines.len() {
643 let next = state.lines.remove(state.cursor_row + 1);
644 state.lines[state.cursor_row].push_str(&next);
645 }
646 consumed_indices.push(i);
647 }
648 KeyCode::End => {
649 state.cursor_col = state.lines[state.cursor_row].chars().count();
650 consumed_indices.push(i);
651 }
652 _ => {}
653 }
654 }
655 if let Event::Paste(ref text) = event {
656 for ch in text.chars() {
657 if ch == '\n' || ch == '\r' {
658 let split_index = byte_index_for_char(
659 &state.lines[state.cursor_row],
660 state.cursor_col,
661 );
662 let remainder = state.lines[state.cursor_row].split_off(split_index);
663 state.cursor_row += 1;
664 state.lines.insert(state.cursor_row, remainder);
665 state.cursor_col = 0;
666 } else {
667 if let Some(max) = state.max_length {
668 let total: usize =
669 state.lines.iter().map(|l| l.chars().count()).sum();
670 if total >= max {
671 break;
672 }
673 }
674 let index = byte_index_for_char(
675 &state.lines[state.cursor_row],
676 state.cursor_col,
677 );
678 state.lines[state.cursor_row].insert(index, ch);
679 state.cursor_col += 1;
680 }
681 }
682 consumed_indices.push(i);
683 }
684 }
685
686 for index in consumed_indices {
687 self.consumed[index] = true;
688 }
689 }
690
691 let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
692 let (cursor_vrow, cursor_vcol) =
693 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
694
695 if cursor_vrow < state.scroll_offset {
696 state.scroll_offset = cursor_vrow;
697 }
698 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
699 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
700 }
701
702 let interaction_id = self.interaction_count;
703 self.interaction_count += 1;
704 let mut response = self.response_for(interaction_id);
705 response.focused = focused;
706 self.commands.push(Command::BeginContainer {
707 direction: Direction::Column,
708 gap: 0,
709 align: Align::Start,
710 align_self: None,
711 justify: Justify::Start,
712 border: None,
713 border_sides: BorderSides::all(),
714 border_style: Style::new().fg(self.theme.border),
715 bg_color: None,
716 padding: Padding::default(),
717 margin: Margin::default(),
718 constraints: Constraints::default(),
719 title: None,
720 grow: 0,
721 group_name: None,
722 });
723
724 for vi in 0..visible_rows as usize {
725 let actual_vi = state.scroll_offset + vi;
726 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
727 let line = &state.lines[vl.logical_row];
728 let text: String = line
729 .chars()
730 .skip(vl.char_start)
731 .take(vl.char_count)
732 .collect();
733 (text, actual_vi == cursor_vrow)
734 } else {
735 (String::new(), false)
736 };
737
738 let mut rendered = seg_text.clone();
739 let mut style = if seg_text.is_empty() {
740 Style::new().fg(self.theme.text_dim)
741 } else {
742 Style::new().fg(self.theme.text)
743 };
744
745 if is_cursor_line && focused {
746 rendered.clear();
747 for (idx, ch) in seg_text.chars().enumerate() {
748 if idx == cursor_vcol {
749 rendered.push('▎');
750 }
751 rendered.push(ch);
752 }
753 if cursor_vcol >= seg_text.chars().count() {
754 rendered.push('▎');
755 }
756 style = Style::new().fg(self.theme.text);
757 }
758
759 self.styled(rendered, style);
760 }
761 self.commands.push(Command::EndContainer);
762 self.last_text_idx = None;
763
764 response.changed = state.lines != old_lines;
765 response
766 }
767
768 pub fn progress(&mut self, ratio: f64) -> &mut Self {
773 self.progress_bar(ratio, 20)
774 }
775
776 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
781 self.progress_bar_colored(ratio, width, self.theme.primary)
782 }
783
784 pub fn progress_bar_colored(&mut self, ratio: f64, width: u32, color: Color) -> &mut Self {
785 let clamped = ratio.clamp(0.0, 1.0);
786 let filled = (clamped * width as f64).round() as u32;
787 let empty = width.saturating_sub(filled);
788 let mut bar = String::new();
789 for _ in 0..filled {
790 bar.push('█');
791 }
792 for _ in 0..empty {
793 bar.push('░');
794 }
795 self.styled(bar, Style::new().fg(color))
796 }
797}
798
799#[cfg(test)]
800mod tests {
801 use super::*;
802 use crate::{EventBuilder, KeyCode, TestBackend};
803
804 #[test]
805 fn text_input_shows_matched_suggestions_for_prefix() {
806 let mut backend = TestBackend::new(40, 10);
807 let mut input = TextInputState::new();
808 input.set_suggestions(vec!["hello".into(), "help".into(), "world".into()]);
809
810 let events = EventBuilder::new().key('h').key('e').key('l').build();
811 backend.run_with_events(events, |ui| {
812 ui.text_input(&mut input);
813 });
814
815 backend.assert_contains("hello");
816 backend.assert_contains("help");
817 assert!(!backend.to_string_trimmed().contains("world"));
818 assert_eq!(input.matched_suggestions().len(), 2);
819 }
820
821 #[test]
822 fn text_input_tab_accepts_top_suggestion() {
823 let mut backend = TestBackend::new(40, 10);
824 let mut input = TextInputState::new();
825 input.set_suggestions(vec!["hello".into(), "help".into(), "world".into()]);
826
827 let events = EventBuilder::new()
828 .key('h')
829 .key('e')
830 .key('l')
831 .key_code(KeyCode::Tab)
832 .build();
833 backend.run_with_events(events, |ui| {
834 ui.text_input(&mut input);
835 });
836
837 assert_eq!(input.value, "hello");
838 assert!(!input.show_suggestions);
839 }
840
841 #[test]
842 fn text_input_empty_value_shows_no_suggestions() {
843 let mut backend = TestBackend::new(40, 10);
844 let mut input = TextInputState::new();
845 input.set_suggestions(vec!["hello".into(), "help".into(), "world".into()]);
846
847 backend.render(|ui| {
848 ui.text_input(&mut input);
849 });
850
851 let rendered = backend.to_string_trimmed();
852 assert!(!rendered.contains("hello"));
853 assert!(!rendered.contains("help"));
854 assert!(!rendered.contains("world"));
855 assert!(input.matched_suggestions().is_empty());
856 assert!(!input.show_suggestions);
857 }
858}