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