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 justify: Justify::Start,
344 border: None,
345 border_sides: BorderSides::all(),
346 border_style: Style::new().fg(self.theme.border),
347 bg_color: None,
348 padding: Padding::default(),
349 margin: Margin::default(),
350 constraints: Constraints::default(),
351 title: None,
352 grow: 0,
353 group_name: None,
354 });
355 for message in state.messages.iter().rev() {
356 let color = match message.level {
357 ToastLevel::Info => self.theme.primary,
358 ToastLevel::Success => self.theme.success,
359 ToastLevel::Warning => self.theme.warning,
360 ToastLevel::Error => self.theme.error,
361 };
362 self.styled(format!(" ● {}", message.text), Style::new().fg(color));
363 }
364 self.commands.push(Command::EndContainer);
365 self.last_text_idx = None;
366
367 self
368 }
369
370 pub fn slider(
382 &mut self,
383 label: &str,
384 value: &mut f64,
385 range: std::ops::RangeInclusive<f64>,
386 ) -> Response {
387 let focused = self.register_focusable();
388 let mut changed = false;
389
390 let start = *range.start();
391 let end = *range.end();
392 let span = (end - start).max(0.0);
393 let step = if span > 0.0 { span / 20.0 } else { 0.0 };
394
395 *value = (*value).clamp(start, end);
396
397 if focused {
398 let mut consumed_indices = Vec::new();
399 for (i, event) in self.events.iter().enumerate() {
400 if let Event::Key(key) = event {
401 if key.kind != KeyEventKind::Press {
402 continue;
403 }
404
405 match key.code {
406 KeyCode::Left | KeyCode::Char('h') => {
407 if step > 0.0 {
408 let next = (*value - step).max(start);
409 if (next - *value).abs() > f64::EPSILON {
410 *value = next;
411 changed = true;
412 }
413 }
414 consumed_indices.push(i);
415 }
416 KeyCode::Right | KeyCode::Char('l') => {
417 if step > 0.0 {
418 let next = (*value + step).min(end);
419 if (next - *value).abs() > f64::EPSILON {
420 *value = next;
421 changed = true;
422 }
423 }
424 consumed_indices.push(i);
425 }
426 _ => {}
427 }
428 }
429 }
430
431 for idx in consumed_indices {
432 self.consumed[idx] = true;
433 }
434 }
435
436 let ratio = if span <= f64::EPSILON {
437 0.0
438 } else {
439 ((*value - start) / span).clamp(0.0, 1.0)
440 };
441
442 let value_text = format_compact_number(*value);
443 let label_width = UnicodeWidthStr::width(label) as u32;
444 let value_width = UnicodeWidthStr::width(value_text.as_str()) as u32;
445 let track_width = self
446 .area_width
447 .saturating_sub(label_width + value_width + 8)
448 .max(10) as usize;
449 let thumb_idx = if track_width <= 1 {
450 0
451 } else {
452 (ratio * (track_width as f64 - 1.0)).round() as usize
453 };
454
455 let mut track = String::with_capacity(track_width);
456 for i in 0..track_width {
457 if i == thumb_idx {
458 track.push('○');
459 } else if i < thumb_idx {
460 track.push('█');
461 } else {
462 track.push('━');
463 }
464 }
465
466 let text_color = self.theme.text;
467 let border_color = self.theme.border;
468 let primary_color = self.theme.primary;
469 let dim_color = self.theme.text_dim;
470 let mut response = self.container().row(|ui| {
471 ui.text(label).fg(text_color);
472 ui.text("[").fg(border_color);
473 ui.text(track).grow(1).fg(primary_color);
474 ui.text("]").fg(border_color);
475 if focused {
476 ui.text(value_text.as_str()).bold().fg(primary_color);
477 } else {
478 ui.text(value_text.as_str()).fg(dim_color);
479 }
480 });
481 response.focused = focused;
482 response.changed = changed;
483 response
484 }
485
486 pub fn textarea(&mut self, state: &mut TextareaState, visible_rows: u32) -> Response {
494 if state.lines.is_empty() {
495 state.lines.push(String::new());
496 }
497 let old_lines = state.lines.clone();
498 state.cursor_row = state.cursor_row.min(state.lines.len().saturating_sub(1));
499 state.cursor_col = state
500 .cursor_col
501 .min(state.lines[state.cursor_row].chars().count());
502
503 let focused = self.register_focusable();
504 let wrap_w = state.wrap_width.unwrap_or(u32::MAX);
505 let wrapping = state.wrap_width.is_some();
506
507 let pre_vlines = textarea_build_visual_lines(&state.lines, wrap_w);
508
509 if focused {
510 let mut consumed_indices = Vec::new();
511 for (i, event) in self.events.iter().enumerate() {
512 if let Event::Key(key) = event {
513 if key.kind != KeyEventKind::Press {
514 continue;
515 }
516 match key.code {
517 KeyCode::Char(ch) => {
518 if let Some(max) = state.max_length {
519 let total: usize =
520 state.lines.iter().map(|line| line.chars().count()).sum();
521 if total >= max {
522 continue;
523 }
524 }
525 let index = byte_index_for_char(
526 &state.lines[state.cursor_row],
527 state.cursor_col,
528 );
529 state.lines[state.cursor_row].insert(index, ch);
530 state.cursor_col += 1;
531 consumed_indices.push(i);
532 }
533 KeyCode::Enter => {
534 let split_index = byte_index_for_char(
535 &state.lines[state.cursor_row],
536 state.cursor_col,
537 );
538 let remainder = state.lines[state.cursor_row].split_off(split_index);
539 state.cursor_row += 1;
540 state.lines.insert(state.cursor_row, remainder);
541 state.cursor_col = 0;
542 consumed_indices.push(i);
543 }
544 KeyCode::Backspace => {
545 if state.cursor_col > 0 {
546 let start = byte_index_for_char(
547 &state.lines[state.cursor_row],
548 state.cursor_col - 1,
549 );
550 let end = byte_index_for_char(
551 &state.lines[state.cursor_row],
552 state.cursor_col,
553 );
554 state.lines[state.cursor_row].replace_range(start..end, "");
555 state.cursor_col -= 1;
556 } else if state.cursor_row > 0 {
557 let current = state.lines.remove(state.cursor_row);
558 state.cursor_row -= 1;
559 state.cursor_col = state.lines[state.cursor_row].chars().count();
560 state.lines[state.cursor_row].push_str(¤t);
561 }
562 consumed_indices.push(i);
563 }
564 KeyCode::Left => {
565 if state.cursor_col > 0 {
566 state.cursor_col -= 1;
567 } else if state.cursor_row > 0 {
568 state.cursor_row -= 1;
569 state.cursor_col = state.lines[state.cursor_row].chars().count();
570 }
571 consumed_indices.push(i);
572 }
573 KeyCode::Right => {
574 let line_len = state.lines[state.cursor_row].chars().count();
575 if state.cursor_col < line_len {
576 state.cursor_col += 1;
577 } else if state.cursor_row + 1 < state.lines.len() {
578 state.cursor_row += 1;
579 state.cursor_col = 0;
580 }
581 consumed_indices.push(i);
582 }
583 KeyCode::Up => {
584 if wrapping {
585 let (vrow, vcol) = textarea_logical_to_visual(
586 &pre_vlines,
587 state.cursor_row,
588 state.cursor_col,
589 );
590 if vrow > 0 {
591 let (lr, lc) =
592 textarea_visual_to_logical(&pre_vlines, vrow - 1, vcol);
593 state.cursor_row = lr;
594 state.cursor_col = lc;
595 }
596 } else if state.cursor_row > 0 {
597 state.cursor_row -= 1;
598 state.cursor_col = state
599 .cursor_col
600 .min(state.lines[state.cursor_row].chars().count());
601 }
602 consumed_indices.push(i);
603 }
604 KeyCode::Down => {
605 if wrapping {
606 let (vrow, vcol) = textarea_logical_to_visual(
607 &pre_vlines,
608 state.cursor_row,
609 state.cursor_col,
610 );
611 if vrow + 1 < pre_vlines.len() {
612 let (lr, lc) =
613 textarea_visual_to_logical(&pre_vlines, vrow + 1, vcol);
614 state.cursor_row = lr;
615 state.cursor_col = lc;
616 }
617 } else if state.cursor_row + 1 < state.lines.len() {
618 state.cursor_row += 1;
619 state.cursor_col = state
620 .cursor_col
621 .min(state.lines[state.cursor_row].chars().count());
622 }
623 consumed_indices.push(i);
624 }
625 KeyCode::Home => {
626 state.cursor_col = 0;
627 consumed_indices.push(i);
628 }
629 KeyCode::Delete => {
630 let line_len = state.lines[state.cursor_row].chars().count();
631 if state.cursor_col < line_len {
632 let start = byte_index_for_char(
633 &state.lines[state.cursor_row],
634 state.cursor_col,
635 );
636 let end = byte_index_for_char(
637 &state.lines[state.cursor_row],
638 state.cursor_col + 1,
639 );
640 state.lines[state.cursor_row].replace_range(start..end, "");
641 } else if state.cursor_row + 1 < state.lines.len() {
642 let next = state.lines.remove(state.cursor_row + 1);
643 state.lines[state.cursor_row].push_str(&next);
644 }
645 consumed_indices.push(i);
646 }
647 KeyCode::End => {
648 state.cursor_col = state.lines[state.cursor_row].chars().count();
649 consumed_indices.push(i);
650 }
651 _ => {}
652 }
653 }
654 if let Event::Paste(ref text) = event {
655 for ch in text.chars() {
656 if ch == '\n' || ch == '\r' {
657 let split_index = byte_index_for_char(
658 &state.lines[state.cursor_row],
659 state.cursor_col,
660 );
661 let remainder = state.lines[state.cursor_row].split_off(split_index);
662 state.cursor_row += 1;
663 state.lines.insert(state.cursor_row, remainder);
664 state.cursor_col = 0;
665 } else {
666 if let Some(max) = state.max_length {
667 let total: usize =
668 state.lines.iter().map(|l| l.chars().count()).sum();
669 if total >= max {
670 break;
671 }
672 }
673 let index = byte_index_for_char(
674 &state.lines[state.cursor_row],
675 state.cursor_col,
676 );
677 state.lines[state.cursor_row].insert(index, ch);
678 state.cursor_col += 1;
679 }
680 }
681 consumed_indices.push(i);
682 }
683 }
684
685 for index in consumed_indices {
686 self.consumed[index] = true;
687 }
688 }
689
690 let vlines = textarea_build_visual_lines(&state.lines, wrap_w);
691 let (cursor_vrow, cursor_vcol) =
692 textarea_logical_to_visual(&vlines, state.cursor_row, state.cursor_col);
693
694 if cursor_vrow < state.scroll_offset {
695 state.scroll_offset = cursor_vrow;
696 }
697 if cursor_vrow >= state.scroll_offset + visible_rows as usize {
698 state.scroll_offset = cursor_vrow + 1 - visible_rows as usize;
699 }
700
701 let interaction_id = self.interaction_count;
702 self.interaction_count += 1;
703 let mut response = self.response_for(interaction_id);
704 response.focused = focused;
705 self.commands.push(Command::BeginContainer {
706 direction: Direction::Column,
707 gap: 0,
708 align: Align::Start,
709 justify: Justify::Start,
710 border: None,
711 border_sides: BorderSides::all(),
712 border_style: Style::new().fg(self.theme.border),
713 bg_color: None,
714 padding: Padding::default(),
715 margin: Margin::default(),
716 constraints: Constraints::default(),
717 title: None,
718 grow: 0,
719 group_name: None,
720 });
721
722 for vi in 0..visible_rows as usize {
723 let actual_vi = state.scroll_offset + vi;
724 let (seg_text, is_cursor_line) = if let Some(vl) = vlines.get(actual_vi) {
725 let line = &state.lines[vl.logical_row];
726 let text: String = line
727 .chars()
728 .skip(vl.char_start)
729 .take(vl.char_count)
730 .collect();
731 (text, actual_vi == cursor_vrow)
732 } else {
733 (String::new(), false)
734 };
735
736 let mut rendered = seg_text.clone();
737 let mut style = if seg_text.is_empty() {
738 Style::new().fg(self.theme.text_dim)
739 } else {
740 Style::new().fg(self.theme.text)
741 };
742
743 if is_cursor_line && focused {
744 rendered.clear();
745 for (idx, ch) in seg_text.chars().enumerate() {
746 if idx == cursor_vcol {
747 rendered.push('▎');
748 }
749 rendered.push(ch);
750 }
751 if cursor_vcol >= seg_text.chars().count() {
752 rendered.push('▎');
753 }
754 style = Style::new().fg(self.theme.text);
755 }
756
757 self.styled(rendered, style);
758 }
759 self.commands.push(Command::EndContainer);
760 self.last_text_idx = None;
761
762 response.changed = state.lines != old_lines;
763 response
764 }
765
766 pub fn progress(&mut self, ratio: f64) -> &mut Self {
771 self.progress_bar(ratio, 20)
772 }
773
774 pub fn progress_bar(&mut self, ratio: f64, width: u32) -> &mut Self {
779 self.progress_bar_colored(ratio, width, self.theme.primary)
780 }
781
782 pub fn progress_bar_colored(&mut self, ratio: f64, width: u32, color: Color) -> &mut Self {
783 let clamped = ratio.clamp(0.0, 1.0);
784 let filled = (clamped * width as f64).round() as u32;
785 let empty = width.saturating_sub(filled);
786 let mut bar = String::new();
787 for _ in 0..filled {
788 bar.push('█');
789 }
790 for _ in 0..empty {
791 bar.push('░');
792 }
793 self.styled(bar, Style::new().fg(color))
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use super::*;
800 use crate::{EventBuilder, KeyCode, TestBackend};
801
802 #[test]
803 fn text_input_shows_matched_suggestions_for_prefix() {
804 let mut backend = TestBackend::new(40, 10);
805 let mut input = TextInputState::new();
806 input.set_suggestions(vec!["hello".into(), "help".into(), "world".into()]);
807
808 let events = EventBuilder::new().key('h').key('e').key('l').build();
809 backend.run_with_events(events, |ui| {
810 ui.text_input(&mut input);
811 });
812
813 backend.assert_contains("hello");
814 backend.assert_contains("help");
815 assert!(!backend.to_string_trimmed().contains("world"));
816 assert_eq!(input.matched_suggestions().len(), 2);
817 }
818
819 #[test]
820 fn text_input_tab_accepts_top_suggestion() {
821 let mut backend = TestBackend::new(40, 10);
822 let mut input = TextInputState::new();
823 input.set_suggestions(vec!["hello".into(), "help".into(), "world".into()]);
824
825 let events = EventBuilder::new()
826 .key('h')
827 .key('e')
828 .key('l')
829 .key_code(KeyCode::Tab)
830 .build();
831 backend.run_with_events(events, |ui| {
832 ui.text_input(&mut input);
833 });
834
835 assert_eq!(input.value, "hello");
836 assert!(!input.show_suggestions);
837 }
838
839 #[test]
840 fn text_input_empty_value_shows_no_suggestions() {
841 let mut backend = TestBackend::new(40, 10);
842 let mut input = TextInputState::new();
843 input.set_suggestions(vec!["hello".into(), "help".into(), "world".into()]);
844
845 backend.render(|ui| {
846 ui.text_input(&mut input);
847 });
848
849 let rendered = backend.to_string_trimmed();
850 assert!(!rendered.contains("hello"));
851 assert!(!rendered.contains("help"));
852 assert!(!rendered.contains("world"));
853 assert!(input.matched_suggestions().is_empty());
854 assert!(!input.show_suggestions);
855 }
856}