1use std::ops::Range;
29use std::time::Duration;
30
31use gpui::prelude::*;
32use gpui::{
33 fill, point, px, size, App, Bounds, ClipboardItem, Context, ElementInputHandler, Entity,
34 FocusHandle, GlobalElementId, Hsla, KeyDownEvent, LayoutId, MouseDownEvent, MouseMoveEvent,
35 MouseUpEvent, PaintQuad, Pixels, Point, ShapedLine, SharedString, Style, TextRun, UnderlineStyle,
36 Window,
37};
38
39use super::edit::TextEdit;
40use super::{apply_nav, KeyOutcome};
41use crate::theme::theme;
42
43const MASK: char = '\u{2022}';
45const MASK_STR: &str = "\u{2022}";
47
48const BLINK: Duration = Duration::from_millis(530);
52
53const SCROLL_PAD: f32 = 2.0;
56
57#[derive(Debug, Default)]
62pub struct LineState {
63 pub(crate) shaped: Option<ShapedLine>,
66 pub(crate) bounds: Option<Bounds<Pixels>>,
67 pub(crate) scroll: Pixels,
69 pub(crate) marked: Option<Range<usize>>,
71 pub(crate) selecting: bool,
73 pub(crate) masked: bool,
75 pub(crate) empty: bool,
77 pub(crate) focused: bool,
78 pub(crate) caret_on: bool,
79 pub(crate) blinking: bool,
81}
82
83impl LineState {
84 pub fn new() -> Self {
85 LineState {
86 caret_on: true,
87 ..Default::default()
88 }
89 }
90
91 fn shaped_byte(&self, edit: &TextEdit, index: usize) -> usize {
95 if self.masked {
96 index.min(edit.len()) * MASK.len_utf8()
97 } else {
98 edit.byte_of(index)
99 }
100 }
101
102 fn char_index(&self, edit: &TextEdit, byte: usize) -> usize {
104 if self.masked {
105 (byte / MASK.len_utf8()).min(edit.len())
106 } else {
107 edit.char_of(byte)
108 }
109 }
110
111 pub(crate) fn index_at(&self, edit: &TextEdit, position: Point<Pixels>) -> Option<usize> {
115 let (bounds, shaped) = (self.bounds?, self.shaped.as_ref()?);
116 if self.empty {
117 return Some(0);
118 }
119 let x = position.x - bounds.left() + self.scroll;
120 Some(self.char_index(edit, shaped.closest_index_for_x(x)))
121 }
122
123 fn wake(&mut self) {
125 self.caret_on = true;
126 }
127}
128
129pub trait LineEditor: 'static + Sized + gpui::EntityInputHandler {
137 fn edit(&self) -> &TextEdit;
138 fn edit_mut(&mut self) -> &mut TextEdit;
139 fn line(&self) -> &LineState;
140 fn line_mut(&mut self) -> &mut LineState;
141 fn line_focus(&self) -> &FocusHandle;
142
143 fn line_masked(&self) -> bool {
145 false
146 }
147
148 fn line_read_only(&self) -> bool {
150 false
151 }
152
153 fn line_max_length(&self) -> Option<usize> {
156 None
157 }
158
159 fn line_filter(&self, text: String) -> String {
164 text
165 }
166
167 fn line_changed(&mut self, cx: &mut Context<Self>);
170}
171
172macro_rules! line_input_handler {
179 ($ty:ty) => {
180 impl ::gpui::EntityInputHandler for $ty {
181 fn text_for_range(
182 &mut self,
183 range_utf16: ::std::ops::Range<usize>,
184 actual: &mut Option<::std::ops::Range<usize>>,
185 _window: &mut ::gpui::Window,
186 _cx: &mut ::gpui::Context<Self>,
187 ) -> Option<String> {
188 let range = $crate::input::line::from_utf16(self.edit(), &range_utf16);
189 actual.replace($crate::input::line::to_utf16(self.edit(), &range));
190 Some($crate::input::line::slice(self.edit(), &range))
191 }
192
193 fn selected_text_range(
194 &mut self,
195 _ignore_disabled: bool,
196 _window: &mut ::gpui::Window,
197 _cx: &mut ::gpui::Context<Self>,
198 ) -> Option<::gpui::UTF16Selection> {
199 Some($crate::input::line::utf16_selection(self.edit()))
200 }
201
202 fn marked_text_range(
203 &self,
204 _window: &mut ::gpui::Window,
205 _cx: &mut ::gpui::Context<Self>,
206 ) -> Option<::std::ops::Range<usize>> {
207 let marked = self.line().marked.clone()?;
208 Some($crate::input::line::to_utf16(self.edit(), &marked))
209 }
210
211 fn unmark_text(&mut self, _window: &mut ::gpui::Window, _cx: &mut ::gpui::Context<Self>) {
212 self.line_mut().marked = None;
213 }
214
215 fn replace_text_in_range(
216 &mut self,
217 range_utf16: Option<::std::ops::Range<usize>>,
218 text: &str,
219 _window: &mut ::gpui::Window,
220 cx: &mut ::gpui::Context<Self>,
221 ) {
222 $crate::input::line::replace(self, range_utf16, text, None, cx);
223 }
224
225 fn replace_and_mark_text_in_range(
226 &mut self,
227 range_utf16: Option<::std::ops::Range<usize>>,
228 text: &str,
229 selected_utf16: Option<::std::ops::Range<usize>>,
230 _window: &mut ::gpui::Window,
231 cx: &mut ::gpui::Context<Self>,
232 ) {
233 $crate::input::line::replace(self, range_utf16, text, Some(selected_utf16), cx);
234 }
235
236 fn bounds_for_range(
237 &mut self,
238 range_utf16: ::std::ops::Range<usize>,
239 bounds: ::gpui::Bounds<::gpui::Pixels>,
240 _window: &mut ::gpui::Window,
241 _cx: &mut ::gpui::Context<Self>,
242 ) -> Option<::gpui::Bounds<::gpui::Pixels>> {
243 $crate::input::line::range_bounds(self, range_utf16, bounds)
244 }
245
246 fn character_index_for_point(
247 &mut self,
248 point: ::gpui::Point<::gpui::Pixels>,
249 _window: &mut ::gpui::Window,
250 _cx: &mut ::gpui::Context<Self>,
251 ) -> Option<usize> {
252 let index = self.line().index_at(self.edit(), point)?;
253 Some($crate::input::line::to_utf16(self.edit(), &(0..index)).end)
254 }
255 }
256 };
257}
258
259pub(crate) use line_input_handler;
260
261pub(crate) fn to_utf16(edit: &TextEdit, range: &Range<usize>) -> Range<usize> {
267 let buffer = edit.chars();
270 let units = |chars: usize| {
271 buffer[..chars.min(buffer.len())]
272 .iter()
273 .map(|c| c.len_utf16())
274 .sum::<usize>()
275 };
276 units(range.start)..units(range.end)
277}
278
279pub(crate) fn from_utf16(edit: &TextEdit, range: &Range<usize>) -> Range<usize> {
280 let buffer = edit.chars();
281 let chars = |units: usize| {
282 let mut seen = 0;
283 for (index, c) in buffer.iter().enumerate() {
284 if seen >= units {
285 return index;
286 }
287 seen += c.len_utf16();
288 }
289 buffer.len()
290 };
291 chars(range.start)..chars(range.end)
292}
293
294pub(crate) fn slice(edit: &TextEdit, range: &Range<usize>) -> String {
295 let buffer = edit.chars();
296 let start = range.start.min(buffer.len());
297 let end = range.end.clamp(start, buffer.len());
298 buffer[start..end].iter().collect()
299}
300
301pub(crate) fn utf16_selection(edit: &TextEdit) -> gpui::UTF16Selection {
302 let (start, end) = edit.selection().unwrap_or((edit.cursor(), edit.cursor()));
303 let reversed = edit.cursor() == start && start != end;
304 gpui::UTF16Selection {
305 range: to_utf16(edit, &(start..end)),
306 reversed,
307 }
308}
309
310fn flatten(text: &str) -> String {
315 text
316 .chars()
317 .map(|c| if c == '\n' || c == '\r' { ' ' } else { c })
318 .filter(|c| !c.is_control())
319 .collect()
320}
321
322pub(crate) fn replace<V: LineEditor>(
325 this: &mut V,
326 range_utf16: Option<Range<usize>>,
327 text: &str,
328 marking: Option<Option<Range<usize>>>,
329 cx: &mut Context<V>,
330) {
331 if this.line_read_only() {
332 return;
333 }
334 let text = this.line_filter(flatten(text));
335
336 let range = range_utf16
337 .map(|r| from_utf16(this.edit(), &r))
338 .or_else(|| this.line().marked.clone())
339 .unwrap_or_else(|| {
340 this
341 .edit()
342 .selection()
343 .map(|(s, e)| s..e)
344 .unwrap_or_else(|| this.edit().cursor()..this.edit().cursor())
345 });
346
347 let text = match this.line_max_length() {
350 Some(max) => {
351 let kept = this.edit().len() - range.len().min(this.edit().len());
352 text.chars().take(max.saturating_sub(kept)).collect()
353 }
354 None => text,
355 };
356
357 let start = range.start;
358 this.edit_mut().replace_range(range, &text);
359 match marking {
360 Some(selection) => {
361 let end = start + text.chars().count();
362 this.line_mut().marked = (!text.is_empty()).then_some(start..end);
363 if let Some(selection) = selection {
364 let selection = from_utf16(this.edit(), &selection);
365 this
366 .edit_mut()
367 .set_selection(start + selection.start, start + selection.end);
368 }
369 }
370 None => this.line_mut().marked = None,
371 }
372 this.line_mut().wake();
373 this.line_changed(cx);
374}
375
376pub(crate) fn range_bounds<V: LineEditor>(
378 this: &V,
379 range_utf16: Range<usize>,
380 bounds: Bounds<Pixels>,
381) -> Option<Bounds<Pixels>> {
382 let shaped = this.line().shaped.as_ref()?;
383 let range = from_utf16(this.edit(), &range_utf16);
384 let x = |index: usize| {
385 bounds.left() + shaped.x_for_index(this.line().shaped_byte(this.edit(), index))
386 - this.line().scroll
387 };
388 Some(Bounds::from_corners(
389 point(x(range.start), bounds.top()),
390 point(x(range.end), bounds.bottom()),
391 ))
392}
393
394pub(crate) fn mouse_down<V: LineEditor>(
400 this: &mut V,
401 event: &MouseDownEvent,
402 window: &mut Window,
403 cx: &mut Context<V>,
404) {
405 window.focus(this.line_focus());
406 let Some(index) = this.line().index_at(this.edit(), event.position) else {
407 cx.notify();
408 return;
409 };
410 match event.click_count {
411 1 if event.modifiers.shift => this.edit_mut().extend_to(index),
412 1 => this.edit_mut().set_cursor(index),
413 2 => {
414 let (start, end) = this.edit().word_at(index);
415 this.edit_mut().set_selection(start, end);
416 }
417 _ => this.edit_mut().select_all(),
418 }
419 this.line_mut().selecting = true;
420 this.line_mut().wake();
421 cx.notify();
422}
423
424pub(crate) fn mouse_move<V: LineEditor>(
426 this: &mut V,
427 event: &MouseMoveEvent,
428 _window: &mut Window,
429 cx: &mut Context<V>,
430) {
431 if !this.line().selecting {
432 return;
433 }
434 if let Some(index) = this.line().index_at(this.edit(), event.position) {
435 this.edit_mut().extend_to(index);
436 cx.notify();
437 }
438}
439
440pub(crate) fn mouse_up<V: LineEditor>(
441 this: &mut V,
442 _event: &MouseUpEvent,
443 _window: &mut Window,
444 cx: &mut Context<V>,
445) {
446 if this.line().selecting {
447 this.line_mut().selecting = false;
448 cx.notify();
449 }
450}
451
452pub(crate) fn wire<V: LineEditor>(
460 element: gpui::Stateful<gpui::Div>,
461 focus: &FocusHandle,
462 cx: &mut Context<V>,
463) -> gpui::Stateful<gpui::Div> {
464 element
465 .track_focus(focus)
466 .cursor(gpui::CursorStyle::IBeam)
467 .on_mouse_down(gpui::MouseButton::Left, cx.listener(mouse_down))
468 .on_mouse_move(cx.listener(mouse_move))
469 .on_mouse_up(gpui::MouseButton::Left, cx.listener(mouse_up))
470 .on_mouse_up_out(gpui::MouseButton::Left, cx.listener(mouse_up))
471}
472
473macro_rules! line_focus_builders {
479 ($ty:ty) => {
480 impl $ty {
481 pub fn tab_index(mut self, index: isize) -> Self {
485 self.focus = self.focus.clone().tab_index(index);
486 self
487 }
488
489 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
491 self.focus = self.focus.clone().tab_stop(tab_stop);
492 self
493 }
494
495 pub fn focus_handle(&self) -> ::gpui::FocusHandle {
497 self.focus.clone()
498 }
499 }
500 };
501}
502
503pub(crate) use line_focus_builders;
504
505pub(crate) fn keys<V: LineEditor>(
515 this: &mut V,
516 event: &KeyDownEvent,
517 window: &mut Window,
518 cx: &mut Context<V>,
519) -> KeyOutcome {
520 let ks = &event.keystroke;
521 let m = &ks.modifiers;
522
523 if ks.key == "tab" && !m.platform && !m.control {
527 if m.shift {
528 window.focus_prev();
529 } else {
530 window.focus_next();
531 }
532 cx.stop_propagation();
533 return KeyOutcome::Edited;
534 }
535
536 if m.platform && !m.alt && !m.control {
537 match ks.key.as_str() {
538 "c" => return copy(this, cx),
539 "x" => return cut(this, cx),
540 "v" => return paste(this, cx),
541 "z" if m.shift => return history(this, false, cx),
542 "z" => return history(this, true, cx),
543 "y" => return history(this, false, cx),
544 _ => {}
545 }
546 }
547
548 if this.line_read_only() && mutates(ks.key.as_str()) {
549 return KeyOutcome::Pass;
550 }
551
552 let outcome = apply_nav(this.edit_mut(), ks);
553 if outcome == KeyOutcome::Edited {
554 this.line_mut().wake();
555 }
556 outcome
557}
558
559fn mutates(key: &str) -> bool {
561 matches!(key, "backspace" | "delete" | "k")
562}
563
564fn copy<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
565 if !this.line_masked() {
566 if let Some(text) = this.edit().selected_text() {
567 cx.write_to_clipboard(ClipboardItem::new_string(text));
568 }
569 }
570 cx.stop_propagation();
571 KeyOutcome::Pass
572}
573
574fn cut<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
575 if this.line_masked() || this.line_read_only() {
576 cx.stop_propagation();
577 return KeyOutcome::Pass;
578 }
579 let Some(text) = this.edit().selected_text() else {
580 cx.stop_propagation();
581 return KeyOutcome::Pass;
582 };
583 cx.write_to_clipboard(ClipboardItem::new_string(text));
584 this.edit_mut().delete_selection();
585 this.line_mut().wake();
586 cx.stop_propagation();
587 KeyOutcome::Edited
588}
589
590fn paste<V: LineEditor>(this: &mut V, cx: &mut Context<V>) -> KeyOutcome {
591 if this.line_read_only() {
592 cx.stop_propagation();
593 return KeyOutcome::Pass;
594 }
595 let Some(text) = cx.read_from_clipboard().and_then(|item| item.text()) else {
596 cx.stop_propagation();
597 return KeyOutcome::Pass;
598 };
599 let text = this.line_filter(flatten(&text));
600 let text = match this.line_max_length() {
601 Some(max) => {
602 let selected = this.edit().selection().map_or(0, |(s, e)| e - s);
603 let room = max.saturating_sub(this.edit().len() - selected);
604 text.chars().take(room).collect()
605 }
606 None => text,
607 };
608 this.edit_mut().break_undo();
609 this.edit_mut().insert(&text);
610 this.edit_mut().break_undo();
611 this.line_mut().wake();
612 cx.stop_propagation();
613 KeyOutcome::Edited
614}
615
616fn history<V: LineEditor>(this: &mut V, undo: bool, cx: &mut Context<V>) -> KeyOutcome {
617 if this.line_read_only() {
618 cx.stop_propagation();
619 return KeyOutcome::Pass;
620 }
621 let changed = if undo {
622 this.edit_mut().undo()
623 } else {
624 this.edit_mut().redo()
625 };
626 this.line_mut().wake();
627 cx.stop_propagation();
628 if changed {
629 KeyOutcome::Edited
630 } else {
631 KeyOutcome::Pass
632 }
633}
634
635pub struct Line<V: LineEditor> {
643 field: Entity<V>,
644 placeholder: SharedString,
645 placeholder_color: Option<Hsla>,
649}
650
651impl<V: LineEditor> Line<V> {
652 pub fn new(field: Entity<V>) -> Self {
653 Line {
654 field,
655 placeholder: SharedString::default(),
656 placeholder_color: None,
657 }
658 }
659
660 pub fn placeholder(mut self, placeholder: impl Into<SharedString>, color: Hsla) -> Self {
662 self.placeholder = placeholder.into();
663 self.placeholder_color = Some(color);
664 self
665 }
666}
667
668pub struct LinePrepaint {
670 shaped: Option<ShapedLine>,
671 caret: Option<PaintQuad>,
672 selection: Option<PaintQuad>,
673 scroll: Pixels,
674}
675
676impl<V: LineEditor> IntoElement for Line<V> {
677 type Element = Self;
678
679 fn into_element(self) -> Self::Element {
680 self
681 }
682}
683
684impl<V: LineEditor> Element for Line<V> {
685 type RequestLayoutState = ();
686 type PrepaintState = LinePrepaint;
687
688 fn id(&self) -> Option<gpui::ElementId> {
689 None
690 }
691
692 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
693 None
694 }
695
696 fn request_layout(
697 &mut self,
698 _id: Option<&GlobalElementId>,
699 _inspector: Option<&gpui::InspectorElementId>,
700 window: &mut Window,
701 cx: &mut App,
702 ) -> (LayoutId, ()) {
703 let mut style = Style::default();
704 style.size.width = gpui::relative(1.0).into();
705 style.size.height = window.line_height().into();
706 (window.request_layout(style, [], cx), ())
707 }
708
709 fn prepaint(
710 &mut self,
711 _id: Option<&GlobalElementId>,
712 _inspector: Option<&gpui::InspectorElementId>,
713 bounds: Bounds<Pixels>,
714 _layout: &mut (),
715 window: &mut Window,
716 cx: &mut App,
717 ) -> LinePrepaint {
718 let t = theme(cx);
722 let text_color = t.text().hsla();
723 let caret_color = t.primary().hsla();
724 let selection_color = t.selection();
725 let dimmed = t.dimmed().hsla();
726
727 let focused = self.field.read(cx).line_focus().is_focused(window);
728 let field = self.field.read(cx);
729 let masked = field.line_masked();
730 let empty = field.edit().is_empty();
731 let cursor = field.edit().cursor();
732 let selection = field.edit().selection();
733 let marked = field.line().marked.clone();
734 let chars = field.edit().len();
735 let caret_on = field.line().caret_on;
736
737 let display: SharedString = if empty {
741 self.placeholder.clone()
742 } else if masked {
743 SharedString::from(MASK_STR.repeat(chars))
744 } else {
745 SharedString::from(field.edit().text())
746 };
747
748 let style = window.text_style();
749 let font_size = style.font_size.to_pixels(window.rem_size());
750 let color = if empty {
751 self.placeholder_color.unwrap_or(dimmed)
752 } else {
753 text_color
754 };
755 let run = TextRun {
756 len: display.len(),
757 font: style.font(),
758 color,
759 background_color: None,
760 underline: None,
761 strikethrough: None,
762 };
763
764 let runs = match marked.filter(|_| !empty) {
767 Some(marked) => {
768 let limit = display.len();
774 let byte = |index: usize| {
775 if masked {
776 index.saturating_mul(MASK.len_utf8())
777 } else {
778 byte_of(&display, index)
779 }
780 .min(limit)
781 };
782 let start = byte(marked.start);
783 let end = byte(marked.end).max(start);
784 vec![
785 TextRun {
786 len: start,
787 ..run.clone()
788 },
789 TextRun {
790 len: end.saturating_sub(start),
791 underline: Some(UnderlineStyle {
792 color: Some(color),
793 thickness: px(1.0),
794 wavy: false,
795 }),
796 ..run.clone()
797 },
798 TextRun {
799 len: display.len().saturating_sub(end),
800 ..run
801 },
802 ]
803 .into_iter()
804 .filter(|run| run.len > 0)
805 .collect()
806 }
807 None => vec![run],
808 };
809
810 let shaped = window
813 .text_system()
814 .shape_line(display.clone(), font_size, &runs, None);
815
816 let byte = |index: usize| {
822 if masked {
823 index.saturating_mul(MASK.len_utf8())
824 } else {
825 byte_of(&display, index)
826 }
827 };
828 let caret_x = if empty {
829 px(0.0)
830 } else {
831 shaped.x_for_index(byte(cursor))
832 };
833 let width = bounds.size.width;
834 let pad = px(SCROLL_PAD);
835 let mut scroll = self.field.read(cx).line().scroll;
836 scroll = scroll.min((shaped.width - width + pad).max(px(0.0)));
839 if caret_x - scroll > width - pad {
840 scroll = caret_x - width + pad;
841 }
842 if caret_x - scroll < px(0.0) {
843 scroll = caret_x;
844 }
845 scroll = scroll.max(px(0.0));
846
847 let quads = if focused && !empty {
848 match selection {
849 Some((start, end)) => (
850 None,
851 Some(fill(
852 Bounds::from_corners(
853 point(
854 bounds.left() + shaped.x_for_index(byte(start)) - scroll,
855 bounds.top(),
856 ),
857 point(
858 bounds.left() + shaped.x_for_index(byte(end)) - scroll,
859 bounds.bottom(),
860 ),
861 ),
862 selection_color,
863 )),
864 ),
865 None => (caret_quad(bounds, caret_x - scroll, caret_color), None),
866 }
867 } else if focused {
868 (caret_quad(bounds, caret_x - scroll, caret_color), None)
869 } else {
870 (None, None)
871 };
872
873 self.field.update(cx, |field, cx| {
874 let state = field.line_mut();
875 state.scroll = scroll;
876 state.masked = masked;
877 state.empty = empty;
878 if state.focused != focused {
879 state.focused = focused;
880 state.caret_on = true;
881 }
882 if focused && !state.blinking {
885 state.blinking = true;
886 blink(cx);
887 }
888 });
889
890 LinePrepaint {
891 shaped: Some(shaped),
892 caret: quads.0.filter(|_| caret_on),
893 selection: quads.1,
894 scroll,
895 }
896 }
897
898 fn paint(
899 &mut self,
900 _id: Option<&GlobalElementId>,
901 _inspector: Option<&gpui::InspectorElementId>,
902 bounds: Bounds<Pixels>,
903 _layout: &mut (),
904 prepaint: &mut LinePrepaint,
905 window: &mut Window,
906 cx: &mut App,
907 ) {
908 let focus = self.field.read(cx).line_focus().clone();
909 window.handle_input(
910 &focus,
911 ElementInputHandler::new(bounds, self.field.clone()),
912 cx,
913 );
914
915 let shaped = prepaint.shaped.take().unwrap_or_default();
916 let origin = point(bounds.origin.x - prepaint.scroll, bounds.origin.y);
917 let parent_mask = window.content_mask().bounds;
922 let mask = Bounds::from_corners(
923 point(bounds.left(), parent_mask.top()),
924 point(bounds.right(), parent_mask.bottom()),
925 );
926 window.with_content_mask(Some(gpui::ContentMask { bounds: mask }), |window| {
927 if let Some(selection) = prepaint.selection.take() {
928 window.paint_quad(selection);
929 }
930 shaped.paint(origin, window.line_height(), window, cx).ok();
931 if let Some(caret) = prepaint.caret.take() {
932 window.paint_quad(caret);
933 }
934 });
935
936 self.field.update(cx, |field, _| {
937 let state = field.line_mut();
938 state.shaped = Some(shaped);
939 state.bounds = Some(bounds);
940 });
941 }
942}
943
944fn caret_quad(bounds: Bounds<Pixels>, x: Pixels, color: Hsla) -> Option<PaintQuad> {
945 Some(fill(
946 Bounds::new(
947 point(bounds.left() + x, bounds.top()),
948 size(px(1.0), bounds.size.height),
949 ),
950 color,
951 ))
952}
953
954fn byte_of(text: &str, index: usize) -> usize {
956 text
957 .char_indices()
958 .nth(index)
959 .map(|(byte, _)| byte)
960 .unwrap_or(text.len())
961}
962
963fn blink<V: LineEditor>(cx: &mut Context<V>) {
966 cx.spawn(async move |field, cx| loop {
967 cx.background_executor().timer(BLINK).await;
968 let running = field
969 .update(cx, |field, cx| {
970 let state = field.line_mut();
971 if !state.focused {
972 state.blinking = false;
973 state.caret_on = true;
974 cx.notify();
975 return false;
976 }
977 state.caret_on = !state.caret_on;
978 cx.notify();
979 true
980 })
981 .unwrap_or(false);
982 if !running {
983 break;
984 }
985 })
986 .detach();
987}