1use alloc::borrow::Cow;
33use alloc::string::{String, ToString};
34use alloc::vec::Vec;
35use core::cell::{Cell, RefCell, RefMut};
36use core::ops::Range;
37
38use denise::Pen;
39use denise::{
40 Color, ElementState, InputEvent, KeyCode, Modifiers, Point, PointerButton, Rect, Role,
41};
42use denise_text::{TextEngine, TextStyle};
43
44use crate::motion::Wake;
45use crate::widget::{
46 Animation, Event, EventCtx, Handled, MeasureCtx, Measured, Offer, PaintCtx, VisualState, Widget,
47};
48use crate::widgets::describe::{
49 Describe, DynDescribe, Group, Mismatch, Property, PropertyKind, Value,
50};
51use crate::widgets::style::{CARET_BLINKS_FOR_MS, DOUBLE_CLICK_MS, muted};
52
53const BLINK_MS: u64 = 500;
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
62pub struct Pos {
63 pub line: usize,
65 pub col: usize,
67}
68
69impl Pos {
70 pub const ZERO: Self = Self { line: 0, col: 0 };
72
73 #[must_use]
75 pub const fn new(line: usize, col: usize) -> Self {
76 Self { line, col }
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct Span {
86 pub start: usize,
88 pub end: usize,
90 pub color: Color,
92}
93
94pub trait TextDocument: 'static {
100 fn line_count(&mut self) -> Option<usize>;
105
106 fn known_lines(&mut self) -> usize;
111
112 fn line(&mut self, n: usize) -> Option<Cow<'_, str>>;
114
115 fn insert(&mut self, at: Pos, text: &str);
117
118 fn delete(&mut self, from: Pos, to: Pos);
120
121 fn undo(&mut self) -> bool {
123 false
124 }
125
126 fn redo(&mut self) -> bool {
128 false
129 }
130
131 fn spans(&mut self, n: usize, out: &mut Vec<Span>) {
135 let _ = (n, out);
136 }
137
138 fn highlights(&mut self, n: usize, line: &str, out: &mut Vec<Range<usize>>) {
145 let _ = (n, line, out);
146 }
147}
148
149#[must_use]
151pub fn end_of(at: Pos, text: &str) -> Pos {
152 match text.rfind('\n') {
153 None => Pos::new(at.line, at.col + text.len()),
154 Some(last) => {
155 let newlines = text.bytes().filter(|&b| b == b'\n').count();
156 Pos::new(at.line + newlines, text.len() - last - 1)
157 }
158 }
159}
160
161#[derive(Clone, Debug)]
163struct Edit {
164 at: Pos,
165 text: String,
166 inserted: bool,
167}
168
169#[derive(Clone, Debug)]
176pub struct TextBuffer {
177 lines: Vec<String>,
178 undo: Vec<Edit>,
179 redo: Vec<Edit>,
180}
181
182impl TextBuffer {
183 #[must_use]
185 pub fn new() -> Self {
186 Self::from_text("")
187 }
188
189 #[must_use]
191 pub fn from_text(text: &str) -> Self {
192 Self {
193 lines: text.split('\n').map(String::from).collect(),
194 undo: Vec::new(),
195 redo: Vec::new(),
196 }
197 }
198
199 #[must_use]
201 pub fn text(&self) -> String {
202 self.lines.join("\n")
203 }
204
205 #[must_use]
207 pub fn lines(&self) -> &[String] {
208 &self.lines
209 }
210
211 fn splice_in(&mut self, at: Pos, text: &str) -> Pos {
213 let line = &mut self.lines[at.line];
214 let tail = line.split_off(at.col);
215 let mut parts = text.split('\n');
216 line.push_str(parts.next().unwrap_or(""));
217 let mut cursor = at.line;
218 for part in parts {
219 cursor += 1;
220 self.lines.insert(cursor, String::from(part));
221 }
222 let end = Pos::new(cursor, self.lines[cursor].len());
223 self.lines[cursor].push_str(&tail);
224 end
225 }
226
227 fn cut_out(&mut self, from: Pos, to: Pos) -> String {
229 if from.line == to.line {
230 return self.lines[from.line].drain(from.col..to.col).collect();
231 }
232 let mut out = self.lines[from.line].split_off(from.col);
233 let kept = self.lines[to.line].split_off(to.col);
234 for line in self.lines.drain(from.line + 1..=to.line) {
235 out.push('\n');
236 out.push_str(&line);
237 }
238 self.lines[from.line].push_str(&kept);
239 out
240 }
241}
242
243impl Default for TextBuffer {
244 fn default() -> Self {
245 Self::new()
246 }
247}
248
249impl TextDocument for TextBuffer {
250 fn line_count(&mut self) -> Option<usize> {
251 Some(self.lines.len())
252 }
253
254 fn known_lines(&mut self) -> usize {
255 self.lines.len()
256 }
257
258 fn line(&mut self, n: usize) -> Option<Cow<'_, str>> {
259 self.lines.get(n).map(|l| Cow::Borrowed(l.as_str()))
260 }
261
262 fn insert(&mut self, at: Pos, text: &str) {
263 if text.is_empty() {
264 return;
265 }
266 self.splice_in(at, text);
267 self.redo.clear();
268 if let Some(last) = self.undo.last_mut()
271 && last.inserted
272 && !last.text.contains('\n')
273 && !text.contains('\n')
274 && end_of(last.at, &last.text) == at
275 {
276 last.text.push_str(text);
277 return;
278 }
279 self.undo.push(Edit {
280 at,
281 text: text.to_string(),
282 inserted: true,
283 });
284 }
285
286 fn delete(&mut self, from: Pos, to: Pos) {
287 if from >= to {
288 return;
289 }
290 let text = self.cut_out(from, to);
291 self.redo.clear();
292 self.undo.push(Edit {
293 at: from,
294 text,
295 inserted: false,
296 });
297 }
298
299 fn undo(&mut self) -> bool {
300 let Some(edit) = self.undo.pop() else {
301 return false;
302 };
303 if edit.inserted {
304 self.cut_out(edit.at, end_of(edit.at, &edit.text));
305 } else {
306 self.splice_in(edit.at, &edit.text);
307 }
308 self.redo.push(edit);
309 true
310 }
311
312 fn redo(&mut self) -> bool {
313 let Some(edit) = self.redo.pop() else {
314 return false;
315 };
316 if edit.inserted {
317 self.splice_in(edit.at, &edit.text);
318 } else {
319 self.cut_out(edit.at, end_of(edit.at, &edit.text));
320 }
321 self.undo.push(edit);
322 true
323 }
324}
325
326#[derive(Clone, Debug, PartialEq, Eq)]
332pub enum ClipboardRequest {
333 Copy(String),
335 Cut(String),
338 Paste,
340}
341
342pub struct TextArea<M, D = TextBuffer> {
370 doc: RefCell<D>,
371 caret: Pos,
372 anchor: Option<Pos>,
375 goal_x: Option<i32>,
378 top: usize,
380 top_px: i32,
384 smooth_scroll: bool,
386 scroll_x: Cell<i32>,
390 content_w: Cell<i32>,
397 reveal_pending: Cell<bool>,
399 wheel_rest: i32,
401 style: TextStyle,
402 gutter: bool,
403 tab_width: u8,
405 read_only: bool,
406 on_change: Option<M>,
407 on_clipboard: Option<fn(ClipboardRequest) -> M>,
408 dragging: bool,
409 thumb_drag: Option<(i32, usize)>,
412 h_thumb_drag: Option<(i32, i32)>,
415 rows_seen: Cell<usize>,
418 last_click: Option<(Pos, u64, u8)>,
421 blink_epoch: u64,
422 caret_on: bool,
423 has_focus: bool,
424}
425
426fn thumb_span(track: i32, shown: usize, total: usize, at: usize, min: i32) -> (i32, i32) {
434 let total = total.max(1) as i64;
435 let shown = shown.max(1) as i64;
436 let len = ((track as i64 * shown / total) as i32)
437 .max(min)
438 .min(track.max(1));
439 let max_at = (total - shown).max(0);
440 let off = if max_at == 0 {
441 0
442 } else {
443 ((track - len) as i64 * (at as i64).min(max_at) / max_at) as i32
444 };
445 (off, len)
446}
447
448impl<M> TextArea<M, TextBuffer> {
449 #[must_use]
451 pub fn from_text(text: &str) -> Self {
452 Self::new(TextBuffer::from_text(text))
453 }
454
455 #[must_use]
457 pub fn text(&self) -> String {
458 self.doc.borrow().text()
459 }
460
461 pub fn set_text(&mut self, text: &str) {
464 *self.doc.get_mut() = TextBuffer::from_text(text);
465 self.caret = Pos::ZERO;
466 self.anchor = None;
467 self.top = 0;
468 self.top_px = 0;
469 self.scroll_x.set(0);
470 self.content_w.set(0);
471 }
472}
473
474impl<M> Default for TextArea<M, TextBuffer> {
475 fn default() -> Self {
476 Self::from_text("")
477 }
478}
479
480impl<M, D: TextDocument> TextArea<M, D> {
481 #[must_use]
483 pub fn new(doc: D) -> Self {
484 Self {
485 doc: RefCell::new(doc),
486 caret: Pos::ZERO,
487 anchor: None,
488 goal_x: None,
489 top: 0,
490 top_px: 0,
491 smooth_scroll: false,
492 scroll_x: Cell::new(0),
493 content_w: Cell::new(0),
494 reveal_pending: Cell::new(false),
495 wheel_rest: 0,
496 style: TextStyle::built_in(16),
497 gutter: true,
498 tab_width: 4,
499 read_only: false,
500 on_change: None,
501 on_clipboard: None,
502 dragging: false,
503 thumb_drag: None,
504 h_thumb_drag: None,
505 rows_seen: Cell::new(0),
506 last_click: None,
507 blink_epoch: 0,
508 caret_on: true,
509 has_focus: false,
510 }
511 }
512
513 #[must_use]
515 pub fn with_change(mut self, message: M) -> Self {
516 self.on_change = Some(message);
517 self
518 }
519
520 #[must_use]
523 pub fn with_clipboard(mut self, message: fn(ClipboardRequest) -> M) -> Self {
524 self.on_clipboard = Some(message);
525 self
526 }
527
528 #[must_use]
530 pub fn with_style(mut self, style: TextStyle) -> Self {
531 self.style = style;
532 self
533 }
534
535 #[must_use]
537 pub fn with_size(mut self, size_px: u16) -> Self {
538 self.style.size_px = size_px;
539 self
540 }
541
542 #[must_use]
544 pub fn with_gutter(mut self, gutter: bool) -> Self {
545 self.gutter = gutter;
546 self
547 }
548
549 #[must_use]
552 pub fn with_tab_width(mut self, columns: u8) -> Self {
553 self.tab_width = columns.max(1);
554 self
555 }
556
557 #[must_use]
559 pub fn with_read_only(mut self, read_only: bool) -> Self {
560 self.read_only = read_only;
561 self
562 }
563
564 #[must_use]
568 pub fn with_smooth_scroll(mut self, smooth: bool) -> Self {
569 self.set_smooth_scroll(smooth);
570 self
571 }
572
573 pub fn document(&self) -> core::cell::Ref<'_, D> {
575 self.doc.borrow()
576 }
577
578 pub fn document_mut(&mut self) -> &mut D {
581 self.doc.get_mut()
582 }
583
584 pub fn set_document(&mut self, doc: D) {
586 *self.doc.get_mut() = doc;
587 self.caret = Pos::ZERO;
588 self.anchor = None;
589 self.top = 0;
590 self.top_px = 0;
591 self.scroll_x.set(0);
592 self.content_w.set(0);
593 }
594
595 #[inline]
597 pub const fn style(&self) -> TextStyle {
598 self.style
599 }
600
601 pub fn set_style(&mut self, style: TextStyle) {
604 self.style = style;
605 self.top_px = 0;
606 self.content_w.set(0);
607 }
608
609 #[inline]
611 pub const fn gutter(&self) -> bool {
612 self.gutter
613 }
614
615 pub fn set_gutter(&mut self, gutter: bool) {
617 self.gutter = gutter;
618 }
619
620 #[inline]
622 pub const fn tab_width(&self) -> u8 {
623 self.tab_width
624 }
625
626 pub fn set_tab_width(&mut self, columns: u8) {
629 self.tab_width = columns.max(1);
630 self.content_w.set(0);
631 }
632
633 #[inline]
635 pub const fn is_read_only(&self) -> bool {
636 self.read_only
637 }
638
639 pub fn set_read_only(&mut self, read_only: bool) {
641 self.read_only = read_only;
642 }
643
644 #[inline]
646 pub const fn smooth_scroll(&self) -> bool {
647 self.smooth_scroll
648 }
649
650 pub fn set_smooth_scroll(&mut self, smooth: bool) {
653 self.smooth_scroll = smooth;
654 if !smooth {
655 self.top_px = 0;
656 self.wheel_rest = 0;
657 }
658 }
659
660 #[inline]
662 pub const fn caret(&self) -> Pos {
663 self.caret
664 }
665
666 pub fn set_caret(&mut self, pos: Pos) {
669 self.caret = self.clamp(pos);
670 self.anchor = None;
671 self.goal_x = None;
672 }
673
674 pub fn selection(&self) -> Option<(Pos, Pos)> {
676 let anchor = self.anchor.filter(|a| *a != self.caret)?;
677 Some((anchor.min(self.caret), anchor.max(self.caret)))
678 }
679
680 pub fn select_all(&mut self) {
682 self.anchor = Some(Pos::ZERO);
683 self.caret = self.last_pos();
684 self.goal_x = None;
685 }
686
687 pub fn selected_text(&self) -> Option<String> {
690 let (from, to) = self.selection()?;
691 let mut doc = self.doc();
692 let mut out = String::new();
693 for n in from.line..=to.line {
694 let Some(line) = doc.line(n) else { break };
695 let start = if n == from.line { from.col } else { 0 };
696 let end = if n == to.line { to.col } else { line.len() };
697 if n != from.line {
698 out.push('\n');
699 }
700 out.push_str(&line[start.min(end)..end]);
701 }
702 Some(out)
703 }
704
705 pub fn insert_text(&mut self, text: &str) {
709 self.replace_selection(text);
710 }
711
712 #[inline]
714 pub const fn top(&self) -> usize {
715 self.top
716 }
717
718 pub fn set_top(&mut self, line: usize) {
720 let last = self.known_lines() - 1;
721 self.top = line.min(last);
722 self.top_px = 0;
723 }
724
725 #[inline]
729 pub const fn top_px(&self) -> i32 {
730 self.top_px
731 }
732
733 pub fn visible_rows(&self) -> usize {
735 self.rows_seen.get()
736 }
737
738 pub fn go_to(&mut self, line: usize) {
742 self.set_caret(Pos::new(line, 0));
743 let rows = self.rows_seen.get();
744 let top = self.caret.line.saturating_sub(rows / 2);
745 self.top = top.min(self.max_top(rows));
746 self.top_px = 0;
747 self.scroll_x.set(0);
748 }
749
750 pub fn select_range(&mut self, from: Pos, to: Pos) {
755 let (from, to) = (self.clamp(from), self.clamp(to));
756 self.anchor = Some(from);
757 self.caret = to;
758 self.goal_x = None;
759 let rows = self.rows_seen.get();
760 let shown = rows > 0 && from.line >= self.top && to.line < self.top + rows;
761 if !shown {
762 let top = from.line.saturating_sub(rows / 2);
763 self.top = top.min(self.max_top(rows));
764 self.top_px = 0;
765 }
766 self.reveal_pending.set(true);
767 }
768
769 pub fn scroll_x(&self) -> i32 {
771 self.scroll_x.get()
772 }
773
774 fn max_top(&self, rows: usize) -> usize {
777 self.known_lines().saturating_sub(rows.max(1))
778 }
779
780 fn doc(&self) -> RefMut<'_, D> {
781 self.doc.borrow_mut()
782 }
783
784 fn line_text(&self, n: usize) -> Option<String> {
787 self.doc().line(n).map(Cow::into_owned)
788 }
789
790 fn line_len(&self, n: usize) -> usize {
791 self.doc().line(n).map_or(0, |l| l.len())
792 }
793
794 fn known_lines(&self) -> usize {
795 self.doc().known_lines().max(1)
796 }
797
798 fn last_pos(&self) -> Pos {
800 let line = self.known_lines() - 1;
801 Pos::new(line, self.line_len(line))
802 }
803
804 fn clamp(&self, pos: Pos) -> Pos {
806 let line = pos.line.min(self.known_lines() - 1);
807 let Some(text) = self.line_text(line) else {
808 return Pos::new(line, 0);
809 };
810 let mut col = pos.col.min(text.len());
811 while !text.is_char_boundary(col) {
812 col -= 1;
813 }
814 Pos::new(line, col)
815 }
816
817 #[inline]
821 const fn pad(&self) -> i32 {
822 self.style.size_px as i32 / 3
823 }
824
825 fn row_height(&self, engine: &TextEngine) -> i32 {
826 engine.line_height(self.style).max(1)
827 }
828
829 fn offset_in(&self, row_h: i32) -> i32 {
832 self.top_px.clamp(0, row_h - 1)
833 }
834
835 fn gutter_width(&self, engine: &mut TextEngine) -> i32 {
837 if !self.gutter {
838 return self.pad();
839 }
840 let mut doc = self.doc();
841 let count = doc.line_count().unwrap_or_else(|| doc.known_lines()).max(1);
842 drop(doc);
843 let digits = count.to_string().len().max(3) as i32;
844 engine.measure_line(self.style, "0").max(1) * digits + self.pad() * 2
845 }
846
847 #[inline]
849 fn bar_width(&self) -> i32 {
850 (self.style.size_px as i32 * 5 / 8).max(6)
851 }
852
853 fn bar_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Rect {
856 let w = self.bar_width().min(bounds.width.max(0));
857 let h = (bounds.height - self.h_bar_height(engine, bounds)).max(0);
858 Rect::new(bounds.right() - w, bounds.y, w, h)
859 }
860
861 fn thumb_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Option<Rect> {
863 let rows = self.rows(engine, bounds);
864 let total = self.known_lines();
865 if total <= rows {
866 return None;
867 }
868 let bar = self.bar_rect(engine, bounds);
869 let (side, end) = (1, 2);
873 let track_h = (bar.height - end * 2).max(1);
874 let (y, h) = thumb_span(track_h, rows, total, self.top, self.bar_width() * 2);
875 Some(Rect::new(
876 bar.x + side,
877 bar.y + end + y,
878 (bar.width - side * 2).max(1),
879 h,
880 ))
881 }
882
883 fn h_bar_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Rect {
886 let h = self.h_bar_height(engine, bounds);
887 let gutter = self.gutter_width(engine);
888 Rect::new(
889 bounds.x + gutter,
890 bounds.bottom() - h,
891 self.text_width(engine, bounds),
892 h,
893 )
894 }
895
896 fn h_thumb_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Option<Rect> {
898 let bar = self.h_bar_rect(engine, bounds);
899 if bar.height == 0 || bar.width <= 0 {
900 return None;
901 }
902 let span = bar.width + self.max_scroll_x(engine, bounds);
905 let (side, end) = (1, 2);
906 let track_w = (bar.width - end * 2).max(1);
907 let (x, w) = thumb_span(
908 track_w,
909 bar.width as usize,
910 span as usize,
911 self.scroll_x.get().max(0) as usize,
912 self.bar_width() * 2,
913 );
914 Some(Rect::new(
915 bar.x + end + x,
916 bar.y + side,
917 w,
918 (bar.height - side * 2).max(1),
919 ))
920 }
921
922 fn h_bar_height(&self, engine: &mut TextEngine, bounds: Rect) -> i32 {
925 let bar = self.bar_width();
926 if self.max_scroll_x(engine, bounds) > 0 && bounds.height > bar {
927 bar
928 } else {
929 0
930 }
931 }
932
933 fn trail(&self) -> i32 {
937 self.caret_width() + self.pad() * 2
938 }
939
940 fn max_scroll_x(&self, engine: &mut TextEngine, bounds: Rect) -> i32 {
945 let width = self.text_width(engine, bounds);
946 let content = self.content_w.get();
947 if content + self.caret_width() > width {
948 (content + self.trail() - width).max(0)
949 } else {
950 0
951 }
952 }
953
954 fn saw_width(&self, width: i32) {
956 if width > self.content_w.get() {
957 self.content_w.set(width);
958 }
959 }
960
961 fn clamp_scroll_x(&self, engine: &mut TextEngine, bounds: Rect) {
964 let max = self.max_scroll_x(engine, bounds);
965 if self.scroll_x.get() > max {
966 self.scroll_x.set(max);
967 }
968 }
969
970 fn text_width(&self, engine: &mut TextEngine, bounds: Rect) -> i32 {
972 let gutter = self.gutter_width(engine);
973 (bounds.width - gutter - self.bar_width()).max(0)
974 }
975
976 fn text_rect(&self, engine: &mut TextEngine, bounds: Rect) -> Rect {
979 let gutter = self.gutter_width(engine);
980 let width = self.text_width(engine, bounds);
981 let height = (bounds.height - self.h_bar_height(engine, bounds)).max(0);
982 Rect::new(bounds.x + gutter, bounds.y, width, height)
983 }
984
985 fn rows(&self, engine: &mut TextEngine, bounds: Rect) -> usize {
987 let height = self.text_rect(engine, bounds).height;
988 (height / self.row_height(engine)).max(1) as usize
989 }
990
991 fn x_of(&self, engine: &mut TextEngine, line: &str, col: usize) -> i32 {
999 self.advance(engine, 0, &line[..col.min(line.len())])
1000 }
1001
1002 fn advance(&self, engine: &mut TextEngine, mut x: i32, text: &str) -> i32 {
1006 for (i, piece) in text.split('\t').enumerate() {
1007 if i > 0 {
1008 x = self.next_stop(engine, x);
1009 }
1010 if !piece.is_empty() {
1011 x += engine.measure_line(self.style, piece);
1012 }
1013 }
1014 x
1015 }
1016
1017 fn next_stop(&self, engine: &mut TextEngine, x: i32) -> i32 {
1020 let stop = engine.measure_line(self.style, " ").max(1) * i32::from(self.tab_width.max(1));
1021 (x.max(0) / stop + 1) * stop
1022 }
1023
1024 fn draw_run(
1027 &self,
1028 engine: &mut TextEngine,
1029 canvas: &mut Pen<'_>,
1030 origin: Point,
1031 mut x: i32,
1032 text: &str,
1033 color: Color,
1034 ) -> i32 {
1035 for (i, piece) in text.split('\t').enumerate() {
1036 if i > 0 {
1037 x = self.next_stop(engine, x);
1038 }
1039 if !piece.is_empty() {
1040 let at = Point::new(origin.x + x, origin.y);
1041 x += engine.draw(canvas, self.style, at, piece, color).width as i32;
1042 }
1043 }
1044 x
1045 }
1046
1047 fn col_at_x(&self, engine: &mut TextEngine, line: &str, x: i32) -> usize {
1049 if x <= 0 || line.is_empty() {
1050 return 0;
1051 }
1052 let bounds: Vec<usize> = line
1053 .char_indices()
1054 .map(|(i, _)| i)
1055 .chain(core::iter::once(line.len()))
1056 .collect();
1057 let (mut lo, mut hi) = (0, bounds.len() - 1);
1060 while lo < hi {
1061 let mid = (lo + hi).div_ceil(2);
1062 if self.x_of(engine, line, bounds[mid]) <= x {
1063 lo = mid;
1064 } else {
1065 hi = mid - 1;
1066 }
1067 }
1068 if lo + 1 < bounds.len() {
1070 let here = self.x_of(engine, line, bounds[lo]);
1071 let next = self.x_of(engine, line, bounds[lo + 1]);
1072 if x - here > next - x {
1073 return bounds[lo + 1];
1074 }
1075 }
1076 bounds[lo]
1077 }
1078
1079 fn pos_at(&self, engine: &mut TextEngine, bounds: Rect, point: Point) -> Pos {
1081 let row_h = self.row_height(engine);
1082 let y = point.y - bounds.y + self.offset_in(row_h);
1083 let row = (y.max(0) / row_h) as usize;
1084 let line = (self.top + row).min(self.known_lines() - 1);
1085 let text = self.line_text(line).unwrap_or_default();
1086 let x = point.x - self.text_rect(engine, bounds).x + self.scroll_x.get();
1087 Pos::new(line, self.col_at_x(engine, &text, x))
1088 }
1089
1090 fn reveal_caret(&mut self, engine: &mut TextEngine, bounds: Rect) {
1092 let rows = self.rows(engine, bounds);
1093 let row_h = self.row_height(engine);
1094 let height = self.text_rect(engine, bounds).height;
1095 let offset = self.offset_in(row_h);
1096 let below = (self.caret.line.saturating_sub(self.top) + 1) as i64 * row_h as i64
1099 - offset as i64
1100 > height.max(row_h) as i64;
1101 if self.caret.line < self.top || (self.caret.line == self.top && offset > 0) {
1102 self.top = self.caret.line;
1103 self.top_px = 0;
1104 } else if below {
1105 self.top = self.caret.line + 1 - rows;
1106 self.top_px = 0;
1107 }
1108 self.reveal_caret_x(engine, bounds);
1109 }
1110
1111 fn reveal_caret_x(&self, engine: &mut TextEngine, bounds: Rect) {
1113 let area = self.text_rect(engine, bounds);
1114 if area.width <= 0 {
1115 return;
1116 }
1117 let Some(line) = self.line_text(self.caret.line) else {
1118 return;
1119 };
1120 let x = self.x_of(engine, &line, self.caret.col);
1121 self.saw_width(x);
1125 let scroll = self.scroll_x.get();
1126 if x < scroll {
1127 self.scroll_x.set(x);
1128 } else if x + self.trail() > scroll + area.width {
1129 self.scroll_x.set(x + self.trail() - area.width);
1130 }
1131 self.clamp_scroll_x(engine, bounds);
1132 }
1133
1134 fn reveal_range_x(&self, engine: &mut TextEngine, bounds: Rect) {
1138 self.reveal_caret_x(engine, bounds);
1139 let Some(anchor) = self.anchor else { return };
1140 if anchor.line != self.caret.line || anchor.col >= self.caret.col {
1141 return;
1142 }
1143 let Some(line) = self.line_text(self.caret.line) else {
1144 return;
1145 };
1146 let width = self.text_rect(engine, bounds).width;
1147 let start = self.x_of(engine, &line, anchor.col);
1148 let end = self.x_of(engine, &line, self.caret.col) + self.caret_width() + self.pad();
1149 if start < self.scroll_x.get() && end - start <= width {
1150 self.scroll_x.set(start);
1151 }
1152 }
1153
1154 #[inline]
1155 fn caret_width(&self) -> i32 {
1156 (i32::from(self.style.size_px) / 10).max(1)
1157 }
1158
1159 fn move_to(&mut self, to: Pos, extend: bool) {
1163 if extend {
1164 self.anchor.get_or_insert(self.caret);
1165 } else {
1166 self.anchor = None;
1167 }
1168 self.caret = to;
1169 }
1170
1171 fn left_of(&self, pos: Pos) -> Pos {
1172 if pos.col > 0 {
1173 let line = self.line_text(pos.line).unwrap_or_default();
1174 Pos::new(pos.line, prev_boundary(&line, pos.col))
1175 } else if pos.line > 0 {
1176 Pos::new(pos.line - 1, self.line_len(pos.line - 1))
1177 } else {
1178 pos
1179 }
1180 }
1181
1182 fn word_left(&self, pos: Pos) -> Pos {
1185 if pos.col == 0 {
1186 return self.left_of(pos);
1187 }
1188 let line = self.line_text(pos.line).unwrap_or_default();
1189 let mut col = pos.col;
1190 while col > 0 && line[..col].chars().next_back().is_some_and(|c| !is_word(c)) {
1191 col = prev_boundary(&line, col);
1192 }
1193 while col > 0 && line[..col].chars().next_back().is_some_and(is_word) {
1194 col = prev_boundary(&line, col);
1195 }
1196 Pos::new(pos.line, col)
1197 }
1198
1199 fn word_right(&self, pos: Pos) -> Pos {
1201 let line = self.line_text(pos.line).unwrap_or_default();
1202 if pos.col >= line.len() {
1203 return self.right_of(pos);
1204 }
1205 let mut col = pos.col;
1206 while col < line.len() && line[col..].chars().next().is_some_and(|c| !is_word(c)) {
1207 col = next_boundary(&line, col);
1208 }
1209 while col < line.len() && line[col..].chars().next().is_some_and(is_word) {
1210 col = next_boundary(&line, col);
1211 }
1212 Pos::new(pos.line, col)
1213 }
1214
1215 fn right_of(&self, pos: Pos) -> Pos {
1216 let line = self.line_text(pos.line).unwrap_or_default();
1217 if pos.col < line.len() {
1218 Pos::new(pos.line, next_boundary(&line, pos.col))
1219 } else if pos.line + 1 < self.known_lines() {
1220 Pos::new(pos.line + 1, 0)
1221 } else {
1222 pos
1223 }
1224 }
1225
1226 fn vertical(&mut self, engine: &mut TextEngine, by: isize) -> Pos {
1228 let goal = match self.goal_x {
1229 Some(x) => x,
1230 None => {
1231 let line = self.line_text(self.caret.line).unwrap_or_default();
1232 let x = self.x_of(engine, &line, self.caret.col);
1233 self.goal_x = Some(x);
1234 x
1235 }
1236 };
1237 let last = self.known_lines() - 1;
1238 let line = self.caret.line.saturating_add_signed(by).min(last);
1239 let text = self.line_text(line).unwrap_or_default();
1240 Pos::new(line, self.col_at_x(engine, &text, goal))
1241 }
1242
1243 fn replace_selection(&mut self, text: &str) {
1247 if let Some((from, to)) = self.selection() {
1248 self.doc().delete(from, to);
1249 self.caret = from;
1250 }
1251 self.anchor = None;
1252 self.goal_x = None;
1253 if !text.is_empty() {
1254 self.doc().insert(self.caret, text);
1255 self.caret = end_of(self.caret, text);
1256 }
1257 }
1258
1259 fn backspace(&mut self) -> bool {
1260 if self.selection().is_some() {
1261 self.replace_selection("");
1262 return true;
1263 }
1264 let from = self.left_of(self.caret);
1265 if from == self.caret {
1266 return false;
1267 }
1268 self.doc().delete(from, self.caret);
1269 self.caret = from;
1270 self.goal_x = None;
1271 true
1272 }
1273
1274 fn delete_forward(&mut self) -> bool {
1275 if self.selection().is_some() {
1276 self.replace_selection("");
1277 return true;
1278 }
1279 let to = self.right_of(self.caret);
1280 if to == self.caret {
1281 return false;
1282 }
1283 self.doc().delete(self.caret, to);
1284 self.goal_x = None;
1285 true
1286 }
1287
1288 fn wake_caret(&mut self, ctx: &mut EventCtx<'_, M>) {
1291 self.blink_epoch = ctx.now_ms;
1292 self.caret_on = true;
1293 ctx.request_animation();
1294 }
1295
1296 fn moved(&mut self, ctx: &mut EventCtx<'_, M>) -> Handled {
1298 self.wake_caret(ctx);
1299 let bounds = ctx.bounds;
1300 self.reveal_caret(ctx.text, bounds);
1301 Handled::Yes
1302 }
1303
1304 fn key(&mut self, code: KeyCode, modifiers: Modifiers, ctx: &mut EventCtx<'_, M>) -> Handled
1305 where
1306 M: Clone,
1307 {
1308 let shift = modifiers.contains(Modifiers::SHIFT);
1309 let primary = modifiers.contains(Modifiers::CTRL) || modifiers.contains(Modifiers::SUPER);
1312 let by_word = modifiers.contains(Modifiers::CTRL) || modifiers.contains(Modifiers::ALT);
1317 let to_line_end = modifiers.contains(Modifiers::SUPER);
1318 let edited = match code {
1319 KeyCode::ArrowLeft => {
1320 let to = if to_line_end {
1321 Pos::new(self.caret.line, 0)
1322 } else if by_word {
1323 self.word_left(self.caret)
1324 } else {
1325 match self.selection() {
1326 Some((from, _)) if !shift => from,
1327 _ => self.left_of(self.caret),
1328 }
1329 };
1330 self.move_to(to, shift);
1331 self.goal_x = None;
1332 return self.moved(ctx);
1333 }
1334 KeyCode::ArrowRight => {
1335 let to = if to_line_end {
1336 Pos::new(self.caret.line, self.line_len(self.caret.line))
1337 } else if by_word {
1338 self.word_right(self.caret)
1339 } else {
1340 match self.selection() {
1341 Some((_, to)) if !shift => to,
1342 _ => self.right_of(self.caret),
1343 }
1344 };
1345 self.move_to(to, shift);
1346 self.goal_x = None;
1347 return self.moved(ctx);
1348 }
1349 KeyCode::ArrowUp | KeyCode::ArrowDown => {
1350 let by = if code == KeyCode::ArrowUp { -1 } else { 1 };
1351 let to = self.vertical(ctx.text, by);
1352 self.move_to(to, shift);
1353 return self.moved(ctx);
1354 }
1355 KeyCode::PageUp | KeyCode::PageDown => {
1356 let rows = self.rows(ctx.text, ctx.bounds) as isize;
1357 let by = if code == KeyCode::PageUp { -rows } else { rows };
1358 let to = self.vertical(ctx.text, by);
1359 self.move_to(to, shift);
1360 let max_top = self.max_top(rows as usize);
1363 self.top = self.top.saturating_add_signed(by).min(max_top);
1364 self.top_px = 0;
1365 return self.moved(ctx);
1366 }
1367 KeyCode::Home => {
1368 let to = if primary {
1369 Pos::ZERO
1370 } else {
1371 Pos::new(self.caret.line, 0)
1372 };
1373 self.move_to(to, shift);
1374 self.goal_x = None;
1375 return self.moved(ctx);
1376 }
1377 KeyCode::End => {
1378 let to = if primary {
1379 self.last_pos()
1380 } else {
1381 Pos::new(self.caret.line, self.line_len(self.caret.line))
1382 };
1383 self.move_to(to, shift);
1384 self.goal_x = None;
1385 return self.moved(ctx);
1386 }
1387 KeyCode::Escape => {
1388 if self.anchor.take().is_none() {
1389 return Handled::No;
1390 }
1391 return Handled::Yes;
1392 }
1393 KeyCode::A if primary => {
1394 self.select_all();
1395 return self.moved(ctx);
1396 }
1397 KeyCode::C if primary => {
1398 return self.clipboard(ctx, false);
1399 }
1400 KeyCode::X if primary => {
1401 return self.clipboard(ctx, true);
1402 }
1403 KeyCode::V if primary => {
1404 if self.read_only {
1405 return Handled::No;
1406 }
1407 let Some(request) = self.on_clipboard else {
1408 return Handled::No;
1409 };
1410 ctx.emit(request(ClipboardRequest::Paste));
1411 return Handled::Yes;
1412 }
1413 KeyCode::Z if primary && !self.read_only => {
1414 let done = if shift {
1415 self.doc().redo()
1416 } else {
1417 self.doc().undo()
1418 };
1419 if !done {
1420 return Handled::No;
1421 }
1422 self.caret = self.clamp(self.caret);
1425 self.anchor = None;
1426 self.goal_x = None;
1427 true
1428 }
1429 KeyCode::Y if primary && !self.read_only => {
1430 if !self.doc().redo() {
1431 return Handled::No;
1432 }
1433 self.caret = self.clamp(self.caret);
1434 self.anchor = None;
1435 self.goal_x = None;
1436 true
1437 }
1438 KeyCode::Backspace if !self.read_only => self.backspace(),
1439 KeyCode::Delete if !self.read_only => self.delete_forward(),
1440 KeyCode::Enter | KeyCode::NumpadEnter if !self.read_only => {
1441 self.replace_selection("\n");
1442 true
1443 }
1444 _ => return Handled::No,
1445 };
1446 if edited && let Some(message) = self.on_change.clone() {
1447 ctx.emit(message);
1448 }
1449 self.moved(ctx)
1450 }
1451
1452 fn clipboard(&mut self, ctx: &mut EventCtx<'_, M>, cut: bool) -> Handled
1454 where
1455 M: Clone,
1456 {
1457 let Some(request) = self.on_clipboard else {
1458 return Handled::No;
1459 };
1460 let Some(text) = self.selected_text() else {
1461 return Handled::No;
1462 };
1463 if cut && !self.read_only {
1464 self.replace_selection("");
1465 ctx.emit(request(ClipboardRequest::Cut(text)));
1466 if let Some(message) = self.on_change.clone() {
1467 ctx.emit(message);
1468 }
1469 return self.moved(ctx);
1470 }
1471 ctx.emit(request(ClipboardRequest::Copy(text)));
1472 Handled::Yes
1473 }
1474
1475 fn press(
1476 &mut self,
1477 position: Point,
1478 modifiers: Modifiers,
1479 ctx: &mut EventCtx<'_, M>,
1480 ) -> Handled {
1481 let bounds = ctx.bounds;
1482 if self.bar_rect(ctx.text, bounds).contains(position) {
1483 return self.press_bar(position, ctx);
1484 }
1485 if self.h_bar_rect(ctx.text, bounds).contains(position) {
1486 return self.press_h_bar(position, ctx);
1487 }
1488 let pos = self.pos_at(ctx.text, bounds, position);
1489 let now = ctx.now_ms;
1490 let count = match self.last_click {
1494 Some((at, when, count)) if at == pos && now.saturating_sub(when) <= DOUBLE_CLICK_MS => {
1495 count % 3 + 1
1496 }
1497 _ => 1,
1498 };
1499 self.last_click = Some((pos, now, count));
1500 match count {
1501 2 => {
1502 let line = self.line_text(pos.line).unwrap_or_default();
1503 let (start, end) = word_at(&line, pos.col);
1504 self.anchor = Some(Pos::new(pos.line, start));
1505 self.caret = Pos::new(pos.line, end);
1506 self.dragging = false;
1507 }
1508 3 => {
1509 self.anchor = Some(Pos::new(pos.line, 0));
1513 self.caret = Pos::new(pos.line, self.line_len(pos.line));
1514 self.dragging = false;
1515 }
1516 _ => {
1517 self.move_to(pos, modifiers.contains(Modifiers::SHIFT));
1518 self.dragging = true;
1519 }
1520 }
1521 self.goal_x = None;
1522 self.moved(ctx)
1523 }
1524
1525 fn press_bar(&mut self, position: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
1527 let bounds = ctx.bounds;
1528 let Some(thumb) = self.thumb_rect(ctx.text, bounds) else {
1529 return Handled::No;
1530 };
1531 let rows = self.rows(ctx.text, bounds);
1532 if thumb.contains(position) {
1533 self.thumb_drag = Some((position.y, self.top));
1534 } else if position.y < thumb.y {
1535 self.top = self.top.saturating_sub(rows);
1536 self.top_px = 0;
1537 } else {
1538 self.top = (self.top + rows).min(self.max_top(rows));
1539 self.top_px = 0;
1540 }
1541 Handled::Yes
1542 }
1543
1544 fn press_h_bar(&mut self, position: Point, ctx: &mut EventCtx<'_, M>) -> Handled {
1547 let bounds = ctx.bounds;
1548 let Some(thumb) = self.h_thumb_rect(ctx.text, bounds) else {
1549 return Handled::No;
1550 };
1551 if thumb.contains(position) {
1552 self.h_thumb_drag = Some((position.x, self.scroll_x.get()));
1553 return Handled::Yes;
1554 }
1555 let page = self.text_width(ctx.text, bounds);
1556 let max = self.max_scroll_x(ctx.text, bounds);
1557 let to = if position.x < thumb.x {
1558 self.scroll_x.get() - page
1559 } else {
1560 self.scroll_x.get() + page
1561 };
1562 self.scroll_x.set(to.clamp(0, max));
1563 Handled::Yes
1564 }
1565
1566 fn drag_h_thumb(&mut self, engine: &mut TextEngine, bounds: Rect, x: i32) -> Handled {
1568 let Some((from_x, from_scroll)) = self.h_thumb_drag else {
1569 return Handled::No;
1570 };
1571 let Some(thumb) = self.h_thumb_rect(engine, bounds) else {
1572 return Handled::No;
1573 };
1574 let max = self.max_scroll_x(engine, bounds);
1575 let travel = (self.h_bar_rect(engine, bounds).width - 4 - thumb.width).max(1) as i64;
1576 let moved = (x - from_x) as i64 * max as i64 / travel;
1577 let scroll = (from_scroll as i64 + moved).clamp(0, max as i64) as i32;
1578 if scroll == self.scroll_x.get() {
1579 return Handled::No;
1580 }
1581 self.scroll_x.set(scroll);
1582 Handled::Yes
1583 }
1584
1585 fn drag_thumb(&mut self, engine: &mut TextEngine, bounds: Rect, y: i32) -> Handled {
1587 let Some((from_y, from_top)) = self.thumb_drag else {
1588 return Handled::No;
1589 };
1590 let Some(thumb) = self.thumb_rect(engine, bounds) else {
1591 return Handled::No;
1592 };
1593 let rows = self.rows(engine, bounds);
1594 let max_top = self.max_top(rows);
1595 let travel = (self.bar_rect(engine, bounds).height - 4 - thumb.height).max(1) as i64;
1596 let moved = (y - from_y) as i64 * max_top as i64 / travel;
1597 let top = (from_top as i64 + moved).clamp(0, max_top as i64) as usize;
1598 if top == self.top && self.top_px == 0 {
1599 return Handled::No;
1600 }
1601 self.top = top;
1602 self.top_px = 0;
1603 Handled::Yes
1604 }
1605
1606 fn wheel(
1607 &mut self,
1608 engine: &mut TextEngine,
1609 bounds: Rect,
1610 delta_x: f32,
1611 delta_y: f32,
1612 ) -> Handled {
1613 let row_h = self.row_height(engine);
1614 let max_top = self.max_top(self.rows(engine, bounds));
1615 let (top, top_px) = if self.smooth_scroll {
1616 let row = row_h as i64;
1621 let at = self.top as i64 * row + self.offset_in(row_h) as i64 + delta_y as i64;
1622 let at = at.clamp(0, max_top as i64 * row);
1623 ((at / row) as usize, (at % row) as i32)
1624 } else {
1625 self.wheel_rest += delta_y as i32;
1626 let lines = self.wheel_rest / row_h;
1627 self.wheel_rest -= lines * row_h;
1628 let top = self.top.saturating_add_signed(lines as isize).min(max_top);
1629 (top, 0)
1630 };
1631 let scroll_x =
1632 (self.scroll_x.get() + delta_x as i32).clamp(0, self.max_scroll_x(engine, bounds));
1633 if top == self.top && top_px == self.top_px && scroll_x == self.scroll_x.get() {
1634 return Handled::No;
1635 }
1636 self.top = top;
1637 self.top_px = top_px;
1638 self.scroll_x.set(scroll_x);
1639 Handled::Yes
1640 }
1641}
1642
1643fn prev_boundary(line: &str, col: usize) -> usize {
1645 line[..col]
1646 .chars()
1647 .next_back()
1648 .map_or(0, |c| col - c.len_utf8())
1649}
1650
1651fn next_boundary(line: &str, col: usize) -> usize {
1653 line[col..]
1654 .chars()
1655 .next()
1656 .map_or(col, |c| col + c.len_utf8())
1657}
1658
1659pub(crate) fn is_word(c: char) -> bool {
1665 c.is_alphanumeric() || c == '_'
1666}
1667
1668fn word_at(line: &str, col: usize) -> (usize, usize) {
1670 let before = line[..col].chars().next_back();
1673 let after = line[col..].chars().next();
1674 let class = match (before, after) {
1675 (Some(b), _) if is_word(b) => true,
1676 (_, Some(a)) => is_word(a),
1677 (Some(b), None) => is_word(b),
1678 (None, None) => return (col, col),
1679 };
1680 let start = line[..col]
1681 .char_indices()
1682 .rev()
1683 .take_while(|(_, c)| is_word(*c) == class)
1684 .last()
1685 .map_or(col, |(i, _)| i);
1686 let end = line[col..]
1687 .char_indices()
1688 .find(|(_, c)| is_word(*c) != class)
1689 .map_or(line.len(), |(i, _)| col + i);
1690 (start, end)
1691}
1692
1693impl<M: Clone + 'static, D: TextDocument> Widget<M> for TextArea<M, D> {
1694 fn describe(&self) -> Option<&dyn DynDescribe> {
1695 Some(self)
1696 }
1697
1698 fn describe_mut(&mut self) -> Option<&mut dyn DynDescribe> {
1699 Some(self)
1700 }
1701
1702 fn measure(&self, ctx: &mut MeasureCtx<'_>, _offered: Offer) -> Measured {
1703 Measured::tall(ctx.text.line_height(self.style).max(1) * 3)
1706 }
1707
1708 fn paint(&self, ctx: &mut PaintCtx<'_>, canvas: &mut Pen<'_>) {
1709 let bounds = ctx.bounds;
1710 let theme = ctx.theme;
1711 let disabled = ctx.state.contains(VisualState::DISABLED);
1712 let focused = ctx.state.contains(VisualState::FOCUSED);
1713 canvas.fill_rect(bounds, theme.color(Role::Base100));
1714
1715 let row_h = self.row_height(ctx.text);
1716 let gutter_w = self.gutter_width(ctx.text);
1717 if self.reveal_pending.take() {
1718 self.reveal_range_x(ctx.text, bounds);
1719 }
1720 self.clamp_scroll_x(ctx.text, bounds);
1723 let area = self.text_rect(ctx.text, bounds);
1724 let text_x = area.x - self.scroll_x.get();
1725 let content = if disabled {
1726 theme.color(Role::Base300)
1727 } else {
1728 theme.color(Role::BaseContent)
1729 };
1730 let dim = muted(theme.color(Role::Base100), content);
1731 let selected = theme.color(Role::Accent).with_alpha(60);
1732 let marked = theme.color(Role::Warning).with_alpha(90);
1733 let space_w = ctx.text.measure_line(self.style, " ").max(1);
1734 let selection = self.selection();
1735
1736 if self.gutter {
1737 let gutter = Rect::new(bounds.x, bounds.y, gutter_w, bounds.height);
1738 canvas.fill_rect(gutter, theme.color(Role::Base200));
1739 }
1740
1741 self.rows_seen.set(self.rows(ctx.text, bounds));
1742 let mut spans = Vec::new();
1743 let mut marks: Vec<Range<usize>> = Vec::new();
1744 let offset = self.offset_in(row_h);
1747 let rows = ((area.height + offset) / row_h + 1).max(1) as usize;
1748 for row in 0..rows {
1749 let n = self.top + row;
1750 let Some(line) = self.line_text(n) else { break };
1751 let y = bounds.y + row as i32 * row_h - offset;
1752 if y >= area.bottom() {
1753 break;
1754 }
1755 let strip = Rect::new(bounds.x, y, bounds.width, row_h);
1756 if !strip.intersects(&canvas.clip()) {
1757 continue;
1758 }
1759
1760 if self.gutter {
1761 let number = (n + 1).to_string();
1762 let w = ctx.text.measure_line(self.style, &number);
1763 let x = bounds.x + gutter_w - self.pad() - w;
1764 let color = if n == self.caret.line { content } else { dim };
1765 let gutter = Rect::new(bounds.x, bounds.y, gutter_w, bounds.height);
1768 let mut numbers = canvas.with_clip(gutter);
1769 ctx.text
1770 .draw(&mut numbers, self.style, Point::new(x, y), &number, color);
1771 }
1772
1773 let mut clipped = canvas.with_clip(area);
1774 marks.clear();
1775 if !disabled {
1776 self.doc().highlights(n, &line, &mut marks);
1777 }
1778 let (mut col, mut x) = (0, 0);
1786 for mark in &marks {
1787 if mark.start < col
1788 || mark.start >= mark.end
1789 || mark.end > line.len()
1790 || !line.is_char_boundary(mark.start)
1791 || !line.is_char_boundary(mark.end)
1792 {
1793 continue;
1794 }
1795 let start = self.advance(ctx.text, x, &line[col..mark.start]);
1796 if text_x + start >= area.right() {
1797 break;
1798 }
1799 let end = self.advance(ctx.text, start, &line[mark.start..mark.end]);
1800 if text_x + end > area.x {
1801 clipped.fill_rect(Rect::new(text_x + start, y, end - start, row_h), marked);
1802 }
1803 (col, x) = (mark.end, end);
1804 }
1805
1806 if let Some((from, to)) = selection
1807 && n >= from.line
1808 && n <= to.line
1809 {
1810 let start = if n == from.line { from.col } else { 0 };
1811 let end = if n == to.line { to.col } else { line.len() };
1812 let x0 = text_x + self.x_of(ctx.text, &line, start);
1813 let mut x1 = text_x + self.x_of(ctx.text, &line, end);
1814 if n < to.line {
1815 x1 += space_w;
1817 }
1818 clipped.fill_rect(Rect::new(x0, y, x1 - x0, row_h), selected);
1819 }
1820
1821 spans.clear();
1822 if !disabled {
1823 self.doc().spans(n, &mut spans);
1824 }
1825 let origin = Point::new(text_x, y);
1826 let mut x = 0;
1827 let mut at = 0;
1828 for span in spans
1829 .iter()
1830 .filter(|s| s.start < s.end && s.end <= line.len())
1831 {
1832 if span.start > at {
1833 let run = &line[at..span.start];
1834 x = self.draw_run(ctx.text, &mut clipped, origin, x, run, content);
1835 }
1836 let run = &line[span.start..span.end];
1837 x = self.draw_run(ctx.text, &mut clipped, origin, x, run, span.color);
1838 at = span.end;
1839 }
1840 if at < line.len() {
1841 x = self.draw_run(ctx.text, &mut clipped, origin, x, &line[at..], content);
1842 }
1843 self.saw_width(x);
1845
1846 if n == self.caret.line && focused && self.caret_on && !disabled {
1847 let x = text_x + self.x_of(ctx.text, &line, self.caret.col);
1848 clipped.fill_rect(
1849 Rect::new(x, y, self.caret_width(), row_h),
1850 theme.color(Role::Accent),
1851 );
1852 }
1853 }
1854
1855 if let Some(thumb) = self.thumb_rect(ctx.text, bounds) {
1860 let bar = self.bar_rect(ctx.text, bounds);
1861 let radius = bar.width / 3;
1862 canvas.fill_rect(bar, theme.color(Role::Base200));
1863 let alpha = if self.thumb_drag.is_some() { 170 } else { 110 };
1864 canvas.fill_rounded_rect(thumb, radius, content.with_alpha(alpha));
1865 }
1866
1867 if let Some(thumb) = self.h_thumb_rect(ctx.text, bounds) {
1871 let bar = self.h_bar_rect(ctx.text, bounds);
1872 let radius = bar.height / 3;
1873 canvas.fill_rect(bar, theme.color(Role::Base200));
1874 let alpha = if self.h_thumb_drag.is_some() {
1875 170
1876 } else {
1877 110
1878 };
1879 canvas.fill_rounded_rect(thumb, radius, content.with_alpha(alpha));
1880 }
1881 }
1882
1883 fn on_event(&mut self, event: &Event<'_>, ctx: &mut EventCtx<'_, M>) -> Handled {
1884 match event {
1885 Event::FocusGained => {
1886 self.has_focus = true;
1887 self.wake_caret(ctx);
1888 Handled::No
1889 }
1890 Event::FocusLost => {
1891 self.has_focus = false;
1892 self.dragging = false;
1893 self.thumb_drag = None;
1894 self.h_thumb_drag = None;
1895 self.wake_caret(ctx);
1896 Handled::No
1897 }
1898 Event::PressCancelled => {
1899 self.dragging = false;
1900 self.thumb_drag = None;
1901 self.h_thumb_drag = None;
1902 Handled::No
1903 }
1904 Event::Input(InputEvent::Text { ch }) if !ch.is_control() => {
1905 if self.read_only {
1906 return Handled::No;
1907 }
1908 let mut buf = [0; 4];
1909 self.replace_selection(ch.encode_utf8(&mut buf));
1910 if let Some(message) = self.on_change.clone() {
1911 ctx.emit(message);
1912 }
1913 self.moved(ctx)
1914 }
1915 Event::Input(InputEvent::Key {
1916 code,
1917 state: ElementState::Down,
1918 modifiers,
1919 ..
1920 }) => self.key(*code, *modifiers, ctx),
1921 Event::Input(InputEvent::PointerButton {
1922 button: PointerButton::Left,
1923 state: ElementState::Down,
1924 position,
1925 modifiers,
1926 }) => self.press(*position, *modifiers, ctx),
1927 Event::Input(InputEvent::PointerButton {
1928 button: PointerButton::Left,
1929 state: ElementState::Up,
1930 ..
1931 }) => {
1932 self.dragging = false;
1933 let held = self.thumb_drag.take().is_some() | self.h_thumb_drag.take().is_some();
1934 if held { Handled::Yes } else { Handled::No }
1936 }
1937 Event::Input(InputEvent::PointerMoved { position }) if self.thumb_drag.is_some() => {
1938 let bounds = ctx.bounds;
1939 self.drag_thumb(ctx.text, bounds, position.y)
1940 }
1941 Event::Input(InputEvent::PointerMoved { position }) if self.h_thumb_drag.is_some() => {
1942 let bounds = ctx.bounds;
1943 self.drag_h_thumb(ctx.text, bounds, position.x)
1944 }
1945 Event::Input(InputEvent::PointerMoved { position }) if self.dragging => {
1946 let bounds = ctx.bounds;
1947 let pos = self.pos_at(ctx.text, bounds, *position);
1948 if pos == self.caret {
1949 return Handled::No;
1950 }
1951 self.move_to(pos, true);
1952 self.goal_x = None;
1953 self.moved(ctx)
1954 }
1955 Event::Input(InputEvent::PointerScroll {
1956 delta_x, delta_y, ..
1957 }) => {
1958 let bounds = ctx.bounds;
1959 self.wheel(ctx.text, bounds, *delta_x, *delta_y)
1960 }
1961 _ => Handled::No,
1962 }
1963 }
1964
1965 fn accepts_pointer(&self) -> bool {
1966 true
1967 }
1968
1969 fn focusable(&self) -> bool {
1970 true
1971 }
1972
1973 fn animate(&mut self, now_ms: u64) -> Animation {
1974 if !self.has_focus {
1975 return Animation::NONE;
1976 }
1977 let elapsed = now_ms.saturating_sub(self.blink_epoch);
1978 if elapsed >= CARET_BLINKS_FOR_MS {
1979 let repaint = !self.caret_on;
1981 self.caret_on = true;
1982 return Animation {
1983 repaint,
1984 next: Wake::Never,
1985 };
1986 }
1987 let on = (elapsed / BLINK_MS).is_multiple_of(2);
1988 let repaint = on != self.caret_on;
1989 self.caret_on = on;
1990 Animation {
1991 repaint,
1992 next: Wake::At(
1993 self.blink_epoch.saturating_add(
1994 (elapsed / BLINK_MS)
1995 .saturating_add(1)
1996 .saturating_mul(BLINK_MS),
1997 ),
1998 ),
1999 }
2000 }
2001
2002 fn snap(&mut self, now_ms: u64) -> Animation {
2004 Widget::<M>::animate(self, now_ms)
2005 }
2006}
2007
2008impl<M, D: TextDocument> Describe for TextArea<M, D> {
2009 const KIND: &'static str = "text-area";
2010 const DOC: &'static str = "Lines of text somebody edits.";
2011 const GROUP: Group = Group::Input;
2012 const ICON: &'static denise::icon::Icon = &super::icons::TEXT_AREA;
2013
2014 const PROPERTIES: &'static [Property] = &[
2015 Property::new(
2016 "gutter",
2017 PropertyKind::Bool,
2018 "Number the lines down the left.",
2019 ),
2020 Property::new(
2021 "read-only",
2022 PropertyKind::Bool,
2023 "Show the text and place a caret in it, but change nothing.",
2024 ),
2025 Property::new(
2026 "size",
2027 PropertyKind::Int { min: 6, max: 96 },
2028 "Text size in logical pixels.",
2029 )
2030 .in_pixels(),
2031 Property::new(
2032 "smooth-scroll",
2033 PropertyKind::Bool,
2034 "Scroll a pixel at a time on the wheel, not a line.",
2035 ),
2036 Property::new(
2037 "tab-width",
2038 PropertyKind::Int { min: 1, max: 16 },
2039 "Columns from one tab stop to the next.",
2040 ),
2041 ];
2042
2043 fn get(&self, name: &str) -> Option<Value> {
2044 Some(match name {
2045 "gutter" => Value::Bool(self.gutter),
2046 "read-only" => Value::Bool(self.read_only),
2047 "size" => Value::Int(i32::from(self.style.size_px)),
2048 "smooth-scroll" => Value::Bool(self.smooth_scroll),
2049 "tab-width" => Value::Int(i32::from(self.tab_width)),
2050 _ => return None,
2051 })
2052 }
2053
2054 fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch> {
2055 match name {
2056 "gutter" => self.gutter = value.as_bool()?,
2057 "read-only" => self.read_only = value.as_bool()?,
2058 "size" => self.style.size_px = value.as_size()?,
2059 "smooth-scroll" => self.set_smooth_scroll(value.as_bool()?),
2060 "tab-width" => self.tab_width = value.as_index()?.clamp(1, 16) as u8,
2061 _ => return Err(Mismatch::Unknown),
2062 }
2063 Ok(())
2064 }
2065}
2066
2067#[cfg(test)]
2068mod tests {
2069 use super::*;
2070
2071 #[test]
2072 fn end_of_counts_lines_and_the_last_column() {
2073 assert_eq!(end_of(Pos::new(2, 3), "ab"), Pos::new(2, 5));
2074 assert_eq!(end_of(Pos::new(2, 3), "a\nbc"), Pos::new(3, 2));
2075 assert_eq!(end_of(Pos::new(2, 3), "\n\n"), Pos::new(4, 0));
2076 }
2077
2078 #[test]
2079 fn a_buffer_inserts_and_deletes_across_lines() {
2080 let mut b = TextBuffer::from_text("one\ntwo");
2081 b.insert(Pos::new(0, 3), " and\na half");
2082 assert_eq!(b.text(), "one and\na half\ntwo");
2083 b.delete(Pos::new(0, 3), Pos::new(2, 1));
2084 assert_eq!(b.text(), "onewo");
2085 assert_eq!(b.line_count(), Some(1));
2086 assert_eq!(b.line(0).as_deref(), Some("onewo"));
2087 assert_eq!(b.line(1), None);
2088 }
2089
2090 #[test]
2091 fn undo_takes_back_a_run_of_typing_at_once() {
2092 let mut b = TextBuffer::new();
2093 for ch in ["h", "i", "\n", "t", "here"] {
2094 let at = end_of(Pos::ZERO, &b.text());
2095 b.insert(at, ch);
2096 }
2097 assert_eq!(b.text(), "hi\nthere");
2098 assert!(b.undo());
2099 assert_eq!(b.text(), "hi\n", "the second line was one run");
2100 assert!(b.undo());
2101 assert_eq!(b.text(), "hi");
2102 assert!(b.undo());
2103 assert_eq!(b.text(), "");
2104 assert!(!b.undo());
2105 assert!(b.redo());
2106 assert!(b.redo());
2107 assert_eq!(b.text(), "hi\n");
2108 b.insert(Pos::new(1, 0), "x");
2109 assert!(!b.redo(), "a new edit drops the redo history");
2110 b.delete(Pos::new(0, 1), Pos::new(1, 1));
2111 assert_eq!(b.text(), "h");
2112 assert!(b.undo());
2113 assert_eq!(b.text(), "hi\nx");
2114 }
2115
2116 #[test]
2117 fn words_are_runs_of_one_class() {
2118 assert_eq!(word_at("let x_1 = f(a)", 5), (4, 7));
2119 assert_eq!(
2120 word_at("let x_1 = f(a)", 7),
2121 (4, 7),
2122 "at the end of a word is in it"
2123 );
2124 assert_eq!(
2125 word_at("let x_1 = f(a)", 11),
2126 (10, 11),
2127 "after `f` is still `f`"
2128 );
2129 assert_eq!(word_at("let x_1 = f(a)", 3), (0, 3));
2130 assert_eq!(word_at("a (b", 2), (1, 3), "a run of non-word characters");
2131 assert_eq!(word_at("", 0), (0, 0));
2132 assert_eq!(word_at("æøå bc", 0), (0, 6));
2133 }
2134
2135 #[test]
2136 fn the_thumb_is_the_rows_share_of_the_lines_and_never_a_sliver() {
2137 assert_eq!(thumb_span(1000, 10, 100, 0, 20), (0, 100));
2139 assert_eq!(thumb_span(1000, 10, 100, 90, 20), (900, 100));
2141 assert_eq!(thumb_span(1000, 10, 100, 45, 20), (450, 100));
2143 assert_eq!(thumb_span(1000, 10, 1_000_000, 0, 20), (0, 20));
2145 assert_eq!(thumb_span(1000, 50, 20, 0, 20), (0, 1000));
2147 assert_eq!(
2148 thumb_span(0, 10, 100, 5, 20),
2149 (0, 1),
2150 "a track with no height"
2151 );
2152 }
2153
2154 #[test]
2155 fn boundaries_step_over_whole_characters() {
2156 let line = "aæb";
2157 assert_eq!(next_boundary(line, 1), 3);
2158 assert_eq!(prev_boundary(line, 3), 1);
2159 assert_eq!(prev_boundary(line, 0), 0);
2160 assert_eq!(next_boundary(line, 4), 4);
2161 }
2162}