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